Compare commits
641 Commits
main
...
codex/mode
| Author | SHA1 | Date | |
|---|---|---|---|
| 226dc95703 | |||
| 5b8409718d | |||
| 073becac14 | |||
| 1483b79061 | |||
| bec8d4623d | |||
| 3f8c25d78b | |||
| ed95b084f0 | |||
| cf92181ae4 | |||
| 67c594129d | |||
| 3ec4f25889 | |||
| 499747173b | |||
| 51458ad0e7 | |||
| dc2d678dac | |||
| f513500b3c | |||
| cf3b8e856d | |||
| fae108d520 | |||
| 6cdf8c13a7 | |||
| f234f69502 | |||
| 3d4d0f99d1 | |||
| 87e51c37be | |||
| 78790e9b52 | |||
| 2fadfdcd3e | |||
| 8acf79dbda | |||
| 0a5e7302bc | |||
| 24c0452229 | |||
| 323abdea57 | |||
| 3f071620dc | |||
| ddadaa0405 | |||
| 65b24d9516 | |||
| 084f58573f | |||
| bc624ceb8d | |||
| 39cc62f41f | |||
| f1f0dd5d03 | |||
| c1724edc47 | |||
| 7b99dabb33 | |||
| 3f98e4e0c5 | |||
| 5dc0bc7342 | |||
| 91f3b7f3dc | |||
| 33e62a1c4a | |||
| d2fb4057ab | |||
| 6251c6d566 | |||
| b8c6e11f41 | |||
| 13f334ae55 | |||
| 493282264d | |||
| 05b721bce6 | |||
| d7c88e6653 | |||
| 20ecffa18c | |||
| 5ab06a42e3 | |||
| 14ea181ec3 | |||
| 1ae623000a | |||
| e82bcb6d56 | |||
| bb05fac00f | |||
| ed05ba453e | |||
| b98635a8bb | |||
| 8348dc5bf8 | |||
| 76f0061840 | |||
| 96a13eec72 | |||
| c3af4518a6 | |||
| a57d9f18f2 | |||
| b46b2c3184 | |||
| ac78358022 | |||
| 14a6fc2e57 | |||
| ea1557f7ea | |||
| 93488d0790 | |||
| 0a01523212 | |||
| 65d084ad8e | |||
| a9d3c63ee0 | |||
| 5838a8f4ce | |||
| b889f26443 | |||
| cc67159784 | |||
| c810cc178b | |||
| 36861cbf97 | |||
| 458f9bef0c | |||
| 2ee6534918 | |||
| e5d5d5f9ce | |||
| 6c58b6bb5d | |||
| 33f21e0a1b | |||
| d69869f720 | |||
| 6cce9dd726 | |||
| 81726d30a5 | |||
| 57c6128d11 | |||
| e37b29296e | |||
| ec5f4b76ec | |||
| 648404eec6 | |||
| 876d19f481 | |||
| 397a6be6d9 | |||
| 46fb8efec4 | |||
| 90f5fb29a6 | |||
| 7dc4f773de | |||
| 85d3fd5b93 | |||
| 34a9e91099 | |||
| a4cc251c68 | |||
| 231dce8875 | |||
| ae24285203 | |||
| 8cd384012f | |||
| 4c0450c87f | |||
| 44f9e7fb68 | |||
| e489b1e28c | |||
| c98130a51f | |||
| c9c3df2733 | |||
| 598495dcc7 | |||
| c38e284be2 | |||
| a0dd313e0c | |||
| 570ccb2bfa | |||
| 4df92b9cd2 | |||
| 058997bd78 | |||
| 9f1a52401a | |||
| baee4b2a08 | |||
| d049d586ed | |||
| 59dd010b5a | |||
| 14a3721e0d | |||
| be4f5b0a31 | |||
| a25ec420fe | |||
| 2c42a1e4d8 | |||
| 48a795822a | |||
| a6b01c2d12 | |||
| b9b0663546 | |||
| 7457b06cf9 | |||
| e89d882022 | |||
| 84373f26e7 | |||
| bfaea5398e | |||
| 24cd14c172 | |||
| 3be0f7468c | |||
| b87927b456 | |||
| adb61795a6 | |||
| 9ac2c541dc | |||
| 22748d9967 | |||
| 32cea98661 | |||
| b32ad1b720 | |||
| bc3d348632 | |||
| 6470c6a6a8 | |||
| 08f6515468 | |||
| 94ce1aec92 | |||
| 935e6972a5 | |||
| 0c41101f5f | |||
| 14ccf67acd | |||
| d60f4d30e2 | |||
| 9b482d7f6b | |||
| a63246f716 | |||
| bbdc746426 | |||
| 4c7c48a22c | |||
| 76c0ed3c10 | |||
| 2e98efa13a | |||
| 7be588d763 | |||
| a44222813f | |||
| c8b55b36f7 | |||
| f3834827b1 | |||
| a03db82307 | |||
| ed9709ade8 | |||
| 9d9b93abb1 | |||
| 772dc7332b | |||
| 9d9c87c0cb | |||
| 09df47879d | |||
| 41279c8743 | |||
| 7575f51c45 | |||
| 6c772a1c84 | |||
| ab36af0a8f | |||
| 5ff2992c0e | |||
| 65c7716d62 | |||
| 59c9b05d6c | |||
| 3101e65dd3 | |||
| 4071919124 | |||
| d963daae70 | |||
| 7a9dd150e3 | |||
| bd416f8473 | |||
| 875a0127d9 | |||
| 3be7171010 | |||
| 3c36be4b43 | |||
| 77268a28fb | |||
| ebc84373e6 | |||
| 2d33f9d928 | |||
| af28da4e83 | |||
| 27e7c60413 | |||
| 6151fb7a3d | |||
| 693923b7bd | |||
| 81898a5dcc | |||
| 9cafc39788 | |||
| ba5c3069e1 | |||
| 9a75782891 | |||
| f4f6eb903e | |||
| d0412e3bf9 | |||
| a9ef2c598c | |||
| d0e023556b | |||
| 7c6c5f3e36 | |||
| d4dad133ea | |||
| ee46a6497f | |||
| 26a2349c5f | |||
| 0e6c61e8a9 | |||
| bdd7a32ff5 | |||
| 308fb13075 | |||
| 0fb3bd09ac | |||
| 26470e0fe8 | |||
| 96d1903cf2 | |||
| ad9b91eeda | |||
| 96ff1c41e2 | |||
| d719a5a5e5 | |||
| 84e63c0d34 | |||
| 421f2713db | |||
| df21d673dd | |||
| 76a8db1ef8 | |||
| 65bf047d77 | |||
| 2641db35ac | |||
| 745a5898da | |||
| 03b999e60f | |||
| 92fa5b224a | |||
| 8f062fb0c4 | |||
| 321e5d6287 | |||
| 35477978e5 | |||
| 8a4ca331cb | |||
| e731c06330 | |||
| 3e5340b696 | |||
| 97fd7de955 | |||
| 1dc2ae4f21 | |||
| 711a9b5037 | |||
| c761cd39fd | |||
| ac4fef8346 | |||
| e17463bf5a | |||
| 0236fc6620 | |||
| c4d00258ff | |||
| b1d71f2621 | |||
| ab3637af9c | |||
| 48f98d337b | |||
| b534c4a4da | |||
| 407297dc2e | |||
| 903fe2d5a1 | |||
| 73564342fc | |||
| d9f294e8e6 | |||
| f225a81ec4 | |||
| fcc0e577b8 | |||
| 808a084ee3 | |||
| f46839bf5c | |||
| e5526c6d0a | |||
| 5def47cdcc | |||
| 062fdaa982 | |||
| a79ef4cda8 | |||
| a104f88360 | |||
| 942c053c19 | |||
| c50ea14a2a | |||
| 32c95b224f | |||
| b825d920d2 | |||
| 1df93c23f7 | |||
| 548b6d3ae5 | |||
| 48a4547f51 | |||
| f7979be80f | |||
| 678bf2dcd6 | |||
| e42afcc83f | |||
| 9373e07d3e | |||
| f42a6540be | |||
| e95861e9b7 | |||
| 31c26c3127 | |||
| d5403f082c | |||
| 75fd7faeb0 | |||
| bd6cdc20c5 | |||
| a9e12f2219 | |||
| 59210c28ea | |||
| 2feeffd6c8 | |||
| 841fbac8eb | |||
| db0ecb590c | |||
| 4ccedaae4c | |||
| c514ac99aa | |||
| 3cd1d46025 | |||
| 111cc8c892 | |||
| c9fb91ab48 | |||
| 2a4698e9f6 | |||
| 9190e9053a | |||
| b8c7cd6e99 | |||
| b65db6f617 | |||
| 7ade927beb | |||
| d0510e9fd2 | |||
| 5aa07b2953 | |||
| d55f26d637 | |||
| 24197c5f7e | |||
| abe3a86cc5 | |||
| 4c61a490ce | |||
| ce787ce186 | |||
| fc20851462 | |||
| 45802dfc7c | |||
| 6440bde002 | |||
| 15c58bfb21 | |||
| b9dbcd10d7 | |||
| f55b1882c0 | |||
| 967a15f15f | |||
| 51601adf6d | |||
| 1b771287f9 | |||
| 0bd1e92ee1 | |||
| f2cb0f2276 | |||
| 1057dd488a | |||
| 11c7d87330 | |||
| 0489c4229e | |||
| b2334e65c9 | |||
| e6831fcb28 | |||
| 63ea626cef | |||
| 08d8c1e82c | |||
| 7aadd1041a | |||
| 4bd29bee9f | |||
| e52fd3cbb5 | |||
| b1acd5118b | |||
| 52cf7628da | |||
| 148aceb705 | |||
| c698de1482 | |||
| 883be98557 | |||
| 401ce33498 | |||
| be4b88dec8 | |||
| 104358bc62 | |||
| cabfa44729 | |||
| dc369c89b0 | |||
| 7d992931d9 | |||
| 6419645e03 | |||
| 3c709f07e6 | |||
| ca452f46e1 | |||
| 576b58b061 | |||
| bc3973ef15 | |||
| 0c7bc98d5b | |||
| 47c35fb859 | |||
| 79942113ef | |||
| 394979e4fc | |||
| 6ab64ccc82 | |||
| 78185b8fd5 | |||
| 2bd1b12ade | |||
| 884a6d4940 | |||
| f8243566c4 | |||
| ca5b94b044 | |||
| 78003923ca | |||
| ab6223c256 | |||
| 8a0810acb3 | |||
| 4528edfb2c | |||
| d980b81bd7 | |||
| 1984b71a0a | |||
| a9ed201adf | |||
| 65e9fdf1b9 | |||
| bd2ee54617 | |||
| a2e795a356 | |||
| c3d85074ac | |||
| 22bbc93b43 | |||
| 7460453b80 | |||
| 855c388027 | |||
| d1bd4e9b46 | |||
| 6945ce7e23 | |||
| 16a1d1e15b | |||
| b184b3e075 | |||
| 10c995f1da | |||
| 921fc8f00b | |||
| 164f99fe48 | |||
| fa1493b843 | |||
| 6b92d0bfea | |||
| 2ac2c45b11 | |||
| b576143afb | |||
| bc5b39057d | |||
| 1369a9048e | |||
| a89f5e6cf2 | |||
| 2ec11e5099 | |||
| 94a6877e7c | |||
| dc23a5648d | |||
| 9adfad9609 | |||
| cee5f141a3 | |||
| 603bb0c4e7 | |||
| 5752bc6ae9 | |||
| 93f3037410 | |||
| 9c7c89fed4 | |||
| 45a7d49d40 | |||
| de9bca8bb5 | |||
| 6427f218e7 | |||
| 6d0cc4eb15 | |||
| a6306c2759 | |||
| 7c76703355 | |||
| 9c3f56954e | |||
| e880f23040 | |||
| defa9fc212 | |||
| ea96f38875 | |||
| b67f3d63cf | |||
| fb111dcdc9 | |||
| 62561624ed | |||
| b5bd6d42f7 | |||
| c640519772 | |||
| fb844f79fd | |||
| 6dac909869 | |||
| 65b262207c | |||
| ef50f4a361 | |||
| 888e94a77c | |||
| c56d301b29 | |||
| 91e1c2c9a3 | |||
| 2087505921 | |||
| 58afa672c7 | |||
| 8dc476d205 | |||
| 73fac0f8e4 | |||
| a487b0ba48 | |||
| efd568a416 | |||
| 4f0909f30c | |||
| fdc1defaba | |||
| 07ed23c2d1 | |||
| 5d5bb24711 | |||
| 21c448d6f1 | |||
| cd9206344d | |||
| 4d06608cc9 | |||
| a64a63def7 | |||
| 19cb14b5dc | |||
| 4de6f496ad | |||
| e1cce05bd6 | |||
| 1ae79ab3c1 | |||
| acdaf3bb8e | |||
| f20595aff6 | |||
| 779d6b0387 | |||
| 3128a0d309 | |||
| ae69f7437f | |||
| 9971b2b7f2 | |||
| 3e15b2f46c | |||
| 7dcf76c3aa | |||
| 155e67fcec | |||
| 2a030318b1 | |||
| 103fe4fb12 | |||
| b2335b1656 | |||
| 692fe08d9f | |||
| 8b12ae35d4 | |||
| 87b1851d59 | |||
| 389cd93e68 | |||
| 6652127545 | |||
| e152616d7f | |||
| ac4d065c78 | |||
| 578b1f6082 | |||
| beb7f717f1 | |||
| 7a9b14a86f | |||
| f3925f8423 | |||
| dd641c047b | |||
| 22006eaf47 | |||
| 537f0dcb2f | |||
| 2ea850cbcc | |||
| e10e16f491 | |||
| 6369c3c969 | |||
| ead7f58285 | |||
| 0e77ca6ba8 | |||
| 1e0500a3f7 | |||
| 4ed72ebc80 | |||
| 6960bd3410 | |||
| 5ee2dd271c | |||
| 5ac807c6bd | |||
| 4af55a7d3f | |||
| 712c28068d | |||
| 777723b68c | |||
| cc3490d9d8 | |||
| d9be3f910a | |||
| 8a7db3bca8 | |||
| 3a78361aea | |||
| 6e3296469a | |||
| 561193b2ab | |||
| 8de9dadf1d | |||
| 853307697a | |||
| fd1772a417 | |||
| 1df506a176 | |||
| b349f24931 | |||
| 5841878df9 | |||
| c8d769c02c | |||
| d28aa25358 | |||
| 76808d60e3 | |||
| 9dd53f9212 | |||
| 0e03e5940a | |||
| e15894e4ea | |||
| 37b1cf82f3 | |||
| 39444af84e | |||
| da584ce0f0 | |||
| 455c91bf29 | |||
| 6fda4d4a90 | |||
| b84dfc049d | |||
| 3a1ca7a8e6 | |||
| b80bd759aa | |||
| a2e47c862e | |||
| 7b882896f1 | |||
| def1a170dc | |||
| 6a3cd867f0 | |||
| 55b725e876 | |||
| d664e9fc39 | |||
| 1dcd96ab36 | |||
| b6a25474ff | |||
| b4c2117992 | |||
| 9a4c595f64 | |||
| ce33eaaef2 | |||
| cc33fbdde2 | |||
| c18297f221 | |||
| 2f8f12a8fd | |||
| 728116da8f | |||
| 36f9e73dd4 | |||
| 9b6c5b0849 | |||
| cc4eaef3e6 | |||
| 77c2a68cc5 | |||
| 647dd81992 | |||
| c5c31f0a56 | |||
| b6c66f3e41 | |||
| 1065183e75 | |||
| dc03491b0d | |||
| 8c99454bf5 | |||
| 0fc73d51d2 | |||
| 831e5deeae | |||
| 22dfde8e7c | |||
| 9a7f4bc0d2 | |||
| 860e5ad31e | |||
| 9b00acec6f | |||
| 53fc5f9a57 | |||
| f6780d183c | |||
| 48fdfd849d | |||
| 52da64fc96 | |||
| 9759abde44 | |||
| 06a44705d0 | |||
| 3ae84de123 | |||
| 8c0784f9c3 | |||
| 995752da75 | |||
| 18617cdbd2 | |||
| 56cb9eaacb | |||
| a5dbf05ab5 | |||
| bbe3db1747 | |||
| 07293c0590 | |||
| 901aff1051 | |||
| 75dd5cfdc9 | |||
| 483bbb4a9c | |||
| 58f163788b | |||
| 8232b0efc8 | |||
| 23c308db1b | |||
| 881b5271a2 | |||
| 952a00e7d3 | |||
| b68ddc42c6 | |||
| 9a7e1c4def | |||
| 5226746c1a | |||
| 5dbeb0504d | |||
| ee3fb36047 | |||
| 1c40602744 | |||
| 818014127a | |||
| d37145660a | |||
| c58b9a3718 | |||
| a6a4e7b249 | |||
| 2b50c2157f | |||
| 0eded78c4c | |||
| 99b2eeb99d | |||
| 7b14c356db | |||
| bad2670f87 | |||
| b3710498f3 | |||
| 1bc90d88b4 | |||
| ddca24779e | |||
| 1ab2a9b846 | |||
| 9c6b52eb8e | |||
| 9d05d193a7 | |||
| e6e80b94ba | |||
| 4e70c90ca8 | |||
| a8faa82b70 | |||
| 4f4ac380ac | |||
| 374cb5b075 | |||
| b0445382dd | |||
| 3701fd2a71 | |||
| 1e4b4cad73 | |||
| a6aa31da79 | |||
| b82cc1e4bd | |||
| 1d44036933 | |||
| 61f86f5aae | |||
| acd8ef6658 | |||
| c22f2e7fa2 | |||
| b7d9dfbf31 | |||
| 92338a0911 | |||
| f7d32f2835 | |||
| 737c29cca4 | |||
| 37a59c01ac | |||
| 7ae37038b3 | |||
| bbe8378630 | |||
| 466c1d0cc0 | |||
| 4a7eff24bf | |||
| f968488e34 | |||
| a12a3454c4 | |||
| 8a92bc973b | |||
| dbaf50cb6e | |||
| 92e9de0441 | |||
| 7280678593 | |||
| b7c087617b | |||
| d8e958769b | |||
| b85c530df7 | |||
| 3823a612ae | |||
| 2a3402e991 | |||
| bbb85bb133 | |||
| d0b0dc3865 | |||
| 6fc8b9e5d2 | |||
| 217450e161 | |||
| 3930f39b14 | |||
| 19f815e3d2 | |||
| 9e0a88726c | |||
| 47eb1ec0b2 | |||
| 0d2a1bd0ae | |||
| 85a5d19a3e | |||
| 02f14f1bf5 | |||
| e00eec30d4 | |||
| 43e3a74c42 | |||
| 75dfc85978 | |||
| 9ce49ef19c | |||
| 36fea6b870 | |||
| 8130a922d0 | |||
| f1e2743d58 | |||
| 7d80afce2f | |||
| 4212387b70 | |||
| bdcd44b340 | |||
| 05064b3a0d | |||
| aa32c47e18 | |||
| 2e0ebd0e13 | |||
| 2754df9f46 | |||
| 9ab73a0354 | |||
| d61c7f37c3 | |||
| ad255a6ddf | |||
| 88507df90e | |||
| 10e5d5b5ae | |||
| c16cab87bd | |||
| 7319cb9aa9 | |||
| 677d0b33a8 | |||
| f1ee1b28a1 | |||
| 2da247f0fb | |||
| 37854ea8b9 | |||
| dc252b2f24 | |||
| d0ef88be89 | |||
| 4ec2d093e8 | |||
| 4eee018367 | |||
| 44aebf61b2 | |||
| f6d3de8cbf | |||
| c62bc4d744 | |||
| 8ebb22325c | |||
| e5d98c2dc3 | |||
| abe578a338 | |||
| 313a360c01 | |||
| 551013c771 | |||
| cc377b5eb5 | |||
| 6c435dafb7 | |||
| 3f5711773e | |||
| a7bb04f54b | |||
| 6604f30ef3 | |||
| 93d8aaaffd | |||
| f9e4bcaeea | |||
| 3d80791245 | |||
| 126280ff7c | |||
| 20b5dba41e | |||
| dfdb7a4468 | |||
| 4d715afd60 | |||
| ac0d0ab49c | |||
| a67e7fc9bb | |||
| 31322bbd83 | |||
| 23eba07901 | |||
| 8014345b99 | |||
| 99eda95cee | |||
| ec5ecbdb54 | |||
| e0ea4597e6 | |||
| c38ff8209b |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -39,6 +39,8 @@ PanoPainterPackage/_pkginfo.txt
|
||||
PanoPainterPackage/AppPackages/
|
||||
PanoPainterPackage/BundleArtifacts/
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
steam/content/
|
||||
steam/output/
|
||||
@@ -53,3 +55,6 @@ linux/Makefile
|
||||
|
||||
webgl/build
|
||||
webgl/.vscode
|
||||
|
||||
out/
|
||||
Testing/
|
||||
|
||||
172
AGENTS.md
Normal file
172
AGENTS.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file is the quick-start map for agents working in this repository. Keep it
|
||||
small and point to the live docs instead of duplicating the whole plan.
|
||||
|
||||
## Current Modernization Goal
|
||||
|
||||
PanoPainter is being incrementally modernized into independently testable C++23
|
||||
components while preserving current behavior. OpenGL remains the working backend
|
||||
until the renderer boundary is proven; Vulkan/Metal/WebGPU work waits behind that
|
||||
boundary.
|
||||
|
||||
Read these first:
|
||||
|
||||
- `docs/modernization/roadmap.md` - live phase roadmap, recent results, and next
|
||||
work queue.
|
||||
- `docs/modernization/debt.md` - required debt log. Every temporary adapter,
|
||||
retained legacy dependency, skipped platform, fallback, or shortcut needs an
|
||||
entry with validation and removal conditions.
|
||||
- `docs/modernization/capability-map.md` - behavior that must be preserved.
|
||||
- `docs/modernization/build-inventory.md` - current build/component inventory.
|
||||
- `docs/modernization/tasks.md` - measurable task tracker and scorecard.
|
||||
- `docs/modernization/director-workflow.md` - multi-agent director/captain
|
||||
workflow; use only when the user asks for subagents or delegation.
|
||||
- `docs/adr/0001-modernization-boundaries.md` - component boundary decisions.
|
||||
|
||||
## Working Rules
|
||||
|
||||
- Work on `codex/modernization-cmake-foundation` unless the user says otherwise.
|
||||
- Commit and push each verified successful progress slice.
|
||||
- Prefer larger coherent slices over tiny checkpoints, but keep docs/debt updated
|
||||
with each slice.
|
||||
- After a verified slice is committed and pushed, reset conversation context
|
||||
before starting the next slice when practical, especially if the thread is
|
||||
approaching automatic compaction. Record all needed resume state in committed
|
||||
code/docs first so the next thread can restart from `AGENTS.md`, roadmap/debt,
|
||||
and git history instead of relying on chat transcript context.
|
||||
- Do not revert user changes. Unrelated untracked notes, such as
|
||||
`docs/human-review-notes.md`, should be left alone unless explicitly requested.
|
||||
- Use CMake as the source of truth. Legacy Visual Studio project files are not the
|
||||
modernization path.
|
||||
- Use `apply_patch` for manual source/doc edits.
|
||||
- For delegated work, follow `docs/modernization/director-workflow.md`: the
|
||||
director keeps integration locally, assigns `gpt-5.4` captains to coherent
|
||||
task groups, and uses lighter workers only for bounded disjoint subtasks.
|
||||
|
||||
## Build And Test
|
||||
|
||||
Primary Windows configure/build:
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-msvc-default
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
Quiet checkpoint validation, preferred when working through Codex token-limited
|
||||
sessions:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"
|
||||
```
|
||||
|
||||
The quiet wrapper writes full command logs under `out/logs/quiet-validation`,
|
||||
prints only a compact summary, and applies editable warning/noise filters from
|
||||
`scripts/automation/quiet-validation-ignore.txt`. If a step fails, read the
|
||||
reported log file instead of rerunning with verbose output.
|
||||
|
||||
Focused fast validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug --output-on-failure
|
||||
```
|
||||
|
||||
Useful targeted pattern:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core|pano_cli_plan" --output-on-failure
|
||||
```
|
||||
|
||||
Apple compile validation runs through the Mac mini SSH alias `panopainter-mac`:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device
|
||||
```
|
||||
|
||||
This is a headless component/test/tool compile gate for macOS, iOS simulator,
|
||||
and iOS device. Signed Apple bundles remain tracked in the debt log.
|
||||
|
||||
If MSVC reports corrupt debug information or stale PDB/object errors, clean the
|
||||
generated build tree target before judging source changes:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-msvc-default --config Debug --target clean
|
||||
```
|
||||
|
||||
## Code Navigation
|
||||
|
||||
Codex has a repo-specific skill named `panopainter-code-navigation`. Agents must
|
||||
use it when necessary: follow this path before broad text search when a task is
|
||||
about C++ symbol identity, symbol families, declarations/definitions, override
|
||||
groups, or platform/backend boundaries. Use it when
|
||||
following C++ symbols, finding symbol families with regular expressions, tracing
|
||||
service/interface wiring, or checking platform/backend boundary usage.
|
||||
Reach for `--name-regex`,
|
||||
`--detail-regex`, or `--path-regex` when looking for generated-style name
|
||||
families, override groups, command/service families, signatures, or
|
||||
platform/backend path slices. Use normal `rg` for docs, build files, comments,
|
||||
string literals, generated command names, or exact non-C++ text.
|
||||
|
||||
Prefer compiler-aware navigation when following C++ symbols across the legacy
|
||||
flat source tree and extracted components:
|
||||
|
||||
```powershell
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
The helper talks to `clangd` using an existing `compile_commands.json`. It
|
||||
defaults to `out/build/windows-clangcl-asan` and then `out/build/android-arm64`;
|
||||
pass `--compile-commands-dir` or set `PP_CLANGD_COMPILE_COMMANDS_DIR` when using
|
||||
another Ninja build tree. Use `--name` and `--max-results` to keep output small.
|
||||
Use `--name-regex` for regex filtering against `qualifiedName`,
|
||||
`--detail-regex` for symbol detail/signature filtering, and `--path-regex` for
|
||||
definition/declaration/implementation/reference location filtering. Regex
|
||||
matching is case-insensitive by default, and `--no-ignore-case` makes it
|
||||
case-sensitive. Run `python scripts/dev/clangd_nav.py self-test` or the
|
||||
`panopainter_clangd_nav_regex_self_test` CTest before relying on regex behavior
|
||||
after tool changes.
|
||||
Treat symbol, hover, declaration, definition, and implementation lookups as the
|
||||
reliable path. Reference lookups are riskier because a one-shot clangd process
|
||||
may not have a complete project index; the helper refuses reference queries
|
||||
unless callers pass `--background-index` for broader best-effort results or
|
||||
`--allow-incomplete-references` for explicitly current-translation-unit-only
|
||||
results. Do not use incomplete reference output as proof that a symbol has no
|
||||
other users.
|
||||
|
||||
## Current Architecture Direction
|
||||
|
||||
The desired component split is documented in the roadmap. Current extracted or
|
||||
in-progress boundaries include:
|
||||
|
||||
- `pp_foundation`
|
||||
- `pp_assets`
|
||||
- `pp_paint`
|
||||
- `pp_document`
|
||||
- `pp_renderer_api`
|
||||
- `pp_renderer_gl`
|
||||
- `pp_paint_renderer`
|
||||
- `pp_app_core`
|
||||
- `pp_panopainter_ui`
|
||||
- `pp_platform_*`
|
||||
- `panopainter_app`
|
||||
|
||||
Legacy UI/app code still exists and is being reduced through pure planners,
|
||||
service interfaces, CLI automation, and CTest coverage. When moving behavior out
|
||||
of a legacy node, preserve current behavior first, add focused tests, document
|
||||
remaining shortcuts in `docs/modernization/debt.md`, then wire the live adapter.
|
||||
|
||||
## Legacy Context
|
||||
|
||||
PanoPainter started as a flat C++17 OpenGL panoramic painting app centered on
|
||||
singletons such as `App::I`, `Canvas::I`, `Settings`, and `WacomTablet::I`.
|
||||
Rendering uses GLAD/OpenGL and shaders in `data/shaders/`. UI is XML/Yoga based
|
||||
with many `Node*` subclasses. Project files are PPI, brush packages are PPBR,
|
||||
and Photoshop brushes import through ABR support.
|
||||
|
||||
Exceptions are disabled in app code. Public modernization APIs should return
|
||||
explicit status/result objects.
|
||||
655
CMakeLists.txt
Normal file
655
CMakeLists.txt
Normal file
@@ -0,0 +1,655 @@
|
||||
cmake_minimum_required(VERSION 3.29)
|
||||
|
||||
project(PanoPainter
|
||||
VERSION 0.0.0
|
||||
DESCRIPTION "Panoramic painting and animation application"
|
||||
LANGUAGES C CXX)
|
||||
|
||||
if(POLICY CMP0091)
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
include(PanoPainterOptions)
|
||||
if(PP_ENABLE_ASAN AND MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
else()
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
include(PanoPainterWarnings)
|
||||
include(PanoPainterSources)
|
||||
include(PanoPainterVersion)
|
||||
include(PanoPainterRuntime)
|
||||
include(PanoPainterPackageTargets)
|
||||
include(PanoPainterPlatformTargets)
|
||||
|
||||
if(PP_ENABLE_CLANG_TIDY)
|
||||
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
if(PP_CLANG_TIDY_EXE)
|
||||
set(CMAKE_CXX_CLANG_TIDY "${PP_CLANG_TIDY_EXE}")
|
||||
else()
|
||||
message(WARNING "PP_ENABLE_CLANG_TIDY is ON but clang-tidy was not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_CPPCHECK)
|
||||
find_program(PP_CPPCHECK_EXE NAMES cppcheck)
|
||||
if(PP_CPPCHECK_EXE)
|
||||
set(CMAKE_CXX_CPPCHECK
|
||||
"${PP_CPPCHECK_EXE}"
|
||||
"--enable=warning,style,performance,portability"
|
||||
"--inline-suppr"
|
||||
"--suppress=missingIncludeSystem")
|
||||
else()
|
||||
message(WARNING "PP_ENABLE_CPPCHECK is ON but cppcheck was not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(pp_project_options INTERFACE)
|
||||
target_compile_features(pp_project_options INTERFACE cxx_std_23)
|
||||
|
||||
add_library(pp_project_warnings INTERFACE)
|
||||
pp_configure_project_warnings(pp_project_warnings)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER
|
||||
"com.omigamedev.panopainter.$(PRODUCT_NAME:rfc1034identifier)")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
|
||||
endif()
|
||||
|
||||
if(PP_USE_VCPKG_TINYXML2)
|
||||
find_package(tinyxml2 CONFIG REQUIRED)
|
||||
add_library(pp_xml_tinyxml2 INTERFACE)
|
||||
target_link_libraries(pp_xml_tinyxml2
|
||||
INTERFACE
|
||||
tinyxml2::tinyxml2)
|
||||
else()
|
||||
add_library(pp_vendor_tinyxml2 STATIC
|
||||
libs/tinyxml2/tinyxml2.cpp)
|
||||
target_include_directories(pp_vendor_tinyxml2
|
||||
SYSTEM PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2")
|
||||
target_link_libraries(pp_vendor_tinyxml2
|
||||
PUBLIC
|
||||
pp_project_options)
|
||||
add_library(pp_xml_tinyxml2 ALIAS pp_vendor_tinyxml2)
|
||||
endif()
|
||||
|
||||
add_custom_target(panopainter_modernization_status
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "PanoPainter modernization scaffold configured."
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "Roadmap: docs/modernization/roadmap.md"
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "Debt log: docs/modernization/debt.md"
|
||||
VERBATIM)
|
||||
|
||||
add_custom_target(panopainter_validate_shaders
|
||||
COMMAND "${CMAKE_COMMAND}"
|
||||
"-DPP_SHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/data/shaders"
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidatePanoPainterShaders.cmake"
|
||||
VERBATIM)
|
||||
|
||||
add_library(pp_foundation STATIC
|
||||
src/foundation/binary_stream.cpp
|
||||
src/foundation/event.cpp
|
||||
src/foundation/log.cpp
|
||||
src/foundation/parse.cpp
|
||||
src/foundation/task_queue.cpp
|
||||
src/foundation/trace.cpp)
|
||||
target_include_directories(pp_foundation
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_foundation
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_assets STATIC
|
||||
src/assets/brush_package.cpp
|
||||
src/assets/image_format.cpp
|
||||
src/assets/image_metadata.cpp
|
||||
src/assets/image_pixels.cpp
|
||||
src/assets/ppi_header.cpp
|
||||
src/assets/settings_document.cpp)
|
||||
target_include_directories(pp_assets
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_assets
|
||||
SYSTEM PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb")
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/assets/image_pixels.cpp
|
||||
PROPERTIES
|
||||
COMPILE_OPTIONS "/analyze-")
|
||||
endif()
|
||||
target_link_libraries(pp_assets
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_paint STATIC
|
||||
src/paint/brush.cpp
|
||||
src/paint/blend.cpp
|
||||
src/paint/stroke.cpp
|
||||
src/paint/stroke_script.cpp)
|
||||
target_include_directories(pp_paint
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_paint
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_document STATIC
|
||||
src/document/document.cpp
|
||||
src/document/ppi_export.cpp
|
||||
src/document/ppi_import.cpp)
|
||||
target_include_directories(pp_document
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_document
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_assets
|
||||
pp_paint
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_renderer_api STATIC
|
||||
src/renderer_api/recording_renderer.cpp
|
||||
src/renderer_api/renderer_api.cpp
|
||||
src/renderer_api/shader_catalog.cpp)
|
||||
target_include_directories(pp_renderer_api
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_renderer_api
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
if(PP_ENABLE_OPENGL)
|
||||
add_library(pp_renderer_gl STATIC
|
||||
src/renderer_gl/command_plan.cpp
|
||||
src/renderer_gl/opengl_capabilities.cpp
|
||||
src/renderer_gl/shader_bindings.cpp)
|
||||
target_include_directories(pp_renderer_gl
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_renderer_gl
|
||||
PUBLIC
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(EMSCRIPTEN)
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_GLES=1
|
||||
PP_RENDERER_GL_RUNTIME_WEB=1)
|
||||
elseif(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_GLES=1)
|
||||
else()
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_DESKTOP=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(pp_paint_renderer STATIC
|
||||
src/paint_renderer/compositor.cpp)
|
||||
target_include_directories(pp_paint_renderer
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_paint_renderer
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_ui_core STATIC
|
||||
src/ui_core/color.cpp
|
||||
src/ui_core/layout_value.cpp
|
||||
src/ui_core/layout_xml.cpp
|
||||
src/ui_core/node_lifetime.cpp
|
||||
src/ui_core/overlay_lifetime.cpp)
|
||||
target_include_directories(pp_ui_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_ui_core
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_xml_tinyxml2
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_platform_api STATIC
|
||||
src/platform_api/asset_file_load_policy.cpp
|
||||
src/platform_api/asset_file_load_policy.h
|
||||
src/platform_api/network_tls_policy.cpp
|
||||
src/platform_api/network_tls_policy.h
|
||||
src/platform_apple/apple_platform_services.cpp
|
||||
src/platform_apple/apple_platform_services.h
|
||||
src/platform_api/platform_policy.cpp
|
||||
src/platform_api/platform_policy.h
|
||||
src/platform_api/platform_services.cpp
|
||||
src/platform_api/platform_services.h)
|
||||
target_include_directories(pp_platform_api
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_platform_api
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_app_core STATIC
|
||||
src/app_core/about_menu.h
|
||||
src/app_core/app_dialog.h
|
||||
src/app_core/app_frame.h
|
||||
src/app_core/app_input.h
|
||||
src/app_core/app_preferences.h
|
||||
src/app_core/app_shutdown.h
|
||||
src/app_core/app_status.h
|
||||
src/app_core/app_startup.h
|
||||
src/app_core/app_thread.h
|
||||
src/app_core/brush_package_import.h
|
||||
src/app_core/brush_package_export.h
|
||||
src/app_core/brush_ui.h
|
||||
src/app_core/canvas_hotkey.h
|
||||
src/app_core/canvas_tool_ui.h
|
||||
src/app_core/canvas_view.h
|
||||
src/app_core/command_convert.h
|
||||
src/app_core/document_animation.h
|
||||
src/app_core/document_canvas.h
|
||||
src/app_core/document_cloud.h
|
||||
src/app_core/document_export.cpp
|
||||
src/app_core/document_import.h
|
||||
src/app_core/document_layer.h
|
||||
src/app_core/document_platform_io.h
|
||||
src/app_core/document_recording.h
|
||||
src/app_core/document_resize.h
|
||||
src/app_core/document_route.cpp
|
||||
src/app_core/document_sharing.h
|
||||
src/app_core/document_session.cpp
|
||||
src/app_core/file_menu.h
|
||||
src/app_core/grid_ui.h
|
||||
src/app_core/history_ui.h
|
||||
src/app_core/main_toolbar.h
|
||||
src/app_core/quick_ui.h
|
||||
src/app_core/tools_menu.h)
|
||||
target_include_directories(pp_app_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_app_core
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
if(PP_BUILD_TOOLS)
|
||||
add_subdirectory(tools/pano_cli)
|
||||
endif()
|
||||
|
||||
if(PP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
if(PP_BUILD_APP)
|
||||
if(WIN32)
|
||||
set(PP_LEGACY_FMT_SOURCES
|
||||
libs/fmt/src/format.cc
|
||||
libs/fmt/src/posix.cc)
|
||||
set(PP_LEGACY_VENDOR_SOURCES ${PP_VENDOR_SOURCES})
|
||||
list(REMOVE_ITEM PP_LEGACY_VENDOR_SOURCES ${PP_LEGACY_FMT_SOURCES})
|
||||
set(PP_LEGACY_VENDOR_DEFINITIONS
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
|
||||
add_library(pp_legacy_vendor OBJECT
|
||||
${PP_LEGACY_VENDOR_SOURCES})
|
||||
target_link_libraries(pp_legacy_vendor
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_vendor
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026")
|
||||
|
||||
add_library(pp_legacy_fmt OBJECT
|
||||
${PP_LEGACY_FMT_SOURCES})
|
||||
target_link_libraries(pp_legacy_fmt
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_fmt
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
if(MSVC_VERSION GREATER_EQUAL 1945)
|
||||
set(PP_FMT_VS2026_COMPAT_HEADER "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026-secure-scl.h")
|
||||
file(WRITE "${PP_FMT_VS2026_COMPAT_HEADER}"
|
||||
"#pragma once\n"
|
||||
"#include <yvals.h>\n"
|
||||
"#ifdef _SECURE_SCL\n"
|
||||
"#undef _SECURE_SCL\n"
|
||||
"#endif\n")
|
||||
target_compile_options(pp_legacy_fmt
|
||||
PUBLIC
|
||||
/FI"${PP_FMT_VS2026_COMPAT_HEADER}")
|
||||
endif()
|
||||
target_compile_definitions(pp_legacy_vendor
|
||||
PUBLIC
|
||||
${PP_LEGACY_VENDOR_DEFINITIONS})
|
||||
target_compile_definitions(pp_legacy_fmt
|
||||
PUBLIC
|
||||
${PP_LEGACY_VENDOR_DEFINITIONS})
|
||||
set_target_properties(pp_legacy_vendor PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
set_target_properties(pp_legacy_fmt PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
add_library(pp_legacy_renderer_gl OBJECT
|
||||
${PP_LEGACY_RENDERER_GL_SOURCES})
|
||||
target_link_libraries(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_renderer_gl PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
target_include_directories(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_renderer_gl PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_renderer_gl PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_assets_io OBJECT
|
||||
${PP_LEGACY_ASSETS_IO_SOURCES})
|
||||
target_link_libraries(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_platform_api
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_assets_io PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_assets_io PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_paint_document OBJECT
|
||||
${PP_LEGACY_PAINT_DOCUMENT_SOURCES})
|
||||
target_link_libraries(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_paint_document PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
target_include_directories(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_paint_document PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_paint_document PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_engine STATIC
|
||||
${PP_LEGACY_ENGINE_SOURCES}
|
||||
$<TARGET_OBJECTS:pp_legacy_assets_io>
|
||||
$<TARGET_OBJECTS:pp_legacy_paint_document>
|
||||
$<TARGET_OBJECTS:pp_legacy_renderer_gl>
|
||||
$<TARGET_OBJECTS:pp_legacy_fmt>
|
||||
$<TARGET_OBJECTS:pp_legacy_vendor>)
|
||||
|
||||
target_link_libraries(pp_legacy_engine
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_engine PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_engine
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_engine
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_engine PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_engine PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_ui_core OBJECT
|
||||
${PP_LEGACY_UI_CORE_SOURCES})
|
||||
|
||||
target_link_libraries(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_engine
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_ui_core PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_ui_core PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_ui_core PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_app STATIC
|
||||
${PP_LEGACY_APP_SOURCES}
|
||||
$<TARGET_OBJECTS:pp_legacy_ui_core>)
|
||||
|
||||
target_link_libraries(pp_legacy_app
|
||||
PUBLIC
|
||||
pp_legacy_engine
|
||||
pp_legacy_ui_core
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_app
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_app
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_app PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_panopainter_ui STATIC
|
||||
${PP_PANOPAINTER_UI_SOURCES})
|
||||
target_link_libraries(pp_panopainter_ui
|
||||
PUBLIC
|
||||
pp_legacy_app
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_platform_api
|
||||
pp_project_warnings)
|
||||
target_precompile_headers(pp_panopainter_ui REUSE_FROM pp_legacy_app)
|
||||
set_target_properties(pp_panopainter_ui PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
add_library(panopainter_app STATIC
|
||||
${PP_PANOPAINTER_APP_SOURCES})
|
||||
target_include_directories(panopainter_app
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(panopainter_app
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_app
|
||||
pp_panopainter_ui
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||
|
||||
add_library(pp_platform_windows OBJECT
|
||||
${PP_WINDOWS_PLATFORM_SOURCES})
|
||||
target_link_libraries(pp_platform_windows
|
||||
PUBLIC
|
||||
panopainter_app
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
|
||||
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
|
||||
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/yuv.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/lib/win64/openvr_api.lib"
|
||||
comdlg32
|
||||
gdi32
|
||||
opengl32
|
||||
ole32
|
||||
shell32
|
||||
shlwapi
|
||||
user32
|
||||
wbemuuid
|
||||
PRIVATE
|
||||
pp_project_options
|
||||
pp_project_warnings)
|
||||
target_precompile_headers(pp_platform_windows REUSE_FROM pp_legacy_app)
|
||||
set_target_properties(pp_platform_windows PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
add_executable(PanoPainter WIN32
|
||||
${PP_WINDOWS_APP_SOURCES}
|
||||
$<TARGET_OBJECTS:pp_platform_windows>)
|
||||
|
||||
target_link_libraries(PanoPainter
|
||||
PRIVATE
|
||||
pp_project_options
|
||||
pp_project_warnings
|
||||
pp_platform_windows)
|
||||
|
||||
set_target_properties(PanoPainter PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
pp_configure_windows_runtime_payloads(PanoPainter)
|
||||
else()
|
||||
message(WARNING "PP_BUILD_APP is enabled, but the root CMake app target is currently Windows-only. Platform alignment is tracked in Phase 6.")
|
||||
endif()
|
||||
endif()
|
||||
297
CMakePresets.json
Normal file
297
CMakePresets.json
Normal file
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"version": 8,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 29,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||
"PP_BUILD_APP": "ON",
|
||||
"PP_BUILD_TESTS": "ON",
|
||||
"PP_BUILD_TOOLS": "ON",
|
||||
"PP_ENABLE_OPENGL": "ON",
|
||||
"PP_ENABLE_VULKAN_EXPERIMENTAL": "OFF",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ENABLE_CLOUD": "ON",
|
||||
"PP_ENABLE_VIDEO": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "platform-headless-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"PP_BUILD_APP": "OFF",
|
||||
"PP_ENABLE_CLOUD": "OFF",
|
||||
"PP_ENABLE_VIDEO": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-vs2026-x64",
|
||||
"inherits": "base",
|
||||
"displayName": "Windows VS 2026 x64",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-default",
|
||||
"inherits": "base",
|
||||
"displayName": "Windows MSVC default generator",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-vcpkg-headless",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Windows MSVC vcpkg headless",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64",
|
||||
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"cacheVariables": {
|
||||
"PP_USE_VCPKG_TINYXML2": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-clangcl-asan",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Windows clang-cl ASan",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang-cl",
|
||||
"CMAKE_CXX_COMPILER": "clang-cl",
|
||||
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL",
|
||||
"PP_ENABLE_ASAN": "ON",
|
||||
"PP_ENABLE_UBSAN": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-clang",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Linux clang",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-base",
|
||||
"hidden": true,
|
||||
"inherits": "platform-headless-base",
|
||||
"generator": "Ninja",
|
||||
"toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
|
||||
"cacheVariables": {
|
||||
"ANDROID_PLATFORM": "android-26",
|
||||
"ANDROID_STL": "c++_shared",
|
||||
"PP_ENABLE_VR": "OFF",
|
||||
"PP_ENABLE_OPENGL": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android x86_64",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "x86_64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-quest-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android Quest arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ANDROID_FLAVOR": "quest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-focus-arm64",
|
||||
"inherits": "android-base",
|
||||
"displayName": "Android Focus/Wave arm64-v8a",
|
||||
"cacheVariables": {
|
||||
"ANDROID_ABI": "arm64-v8a",
|
||||
"PP_ENABLE_VR": "ON",
|
||||
"PP_ANDROID_FLAVOR": "focus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "emscripten",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Emscripten WebGL",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"PP_ENABLE_VR": "OFF",
|
||||
"PP_ENABLE_VIDEO": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "macos",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "macOS",
|
||||
"generator": "Ninja"
|
||||
},
|
||||
{
|
||||
"name": "ios-device",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "iOS device",
|
||||
"generator": "Xcode",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "iOS",
|
||||
"CMAKE_OSX_SYSROOT": "iphoneos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ios-simulator",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "iOS simulator",
|
||||
"generator": "Xcode",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "iOS",
|
||||
"CMAKE_OSX_SYSROOT": "iphonesimulator"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "windows-vs2026-x64",
|
||||
"configurePreset": "windows-vs2026-x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-default",
|
||||
"configurePreset": "windows-msvc-default"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-vcpkg-headless",
|
||||
"configurePreset": "windows-msvc-vcpkg-headless"
|
||||
},
|
||||
{
|
||||
"name": "windows-clangcl-asan",
|
||||
"configurePreset": "windows-clangcl-asan"
|
||||
},
|
||||
{
|
||||
"name": "linux-clang",
|
||||
"configurePreset": "linux-clang"
|
||||
},
|
||||
{
|
||||
"name": "android-arm64",
|
||||
"configurePreset": "android-arm64"
|
||||
},
|
||||
{
|
||||
"name": "android-x64",
|
||||
"configurePreset": "android-x64"
|
||||
},
|
||||
{
|
||||
"name": "android-quest-arm64",
|
||||
"configurePreset": "android-quest-arm64"
|
||||
},
|
||||
{
|
||||
"name": "android-focus-arm64",
|
||||
"configurePreset": "android-focus-arm64"
|
||||
},
|
||||
{
|
||||
"name": "emscripten",
|
||||
"configurePreset": "emscripten"
|
||||
},
|
||||
{
|
||||
"name": "macos",
|
||||
"configurePreset": "macos"
|
||||
},
|
||||
{
|
||||
"name": "ios-device",
|
||||
"configurePreset": "ios-device"
|
||||
},
|
||||
{
|
||||
"name": "ios-simulator",
|
||||
"configurePreset": "ios-simulator"
|
||||
}
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "desktop-fast",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-fast-vs2026",
|
||||
"configurePreset": "windows-vs2026-x64",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-fast-vcpkg",
|
||||
"configurePreset": "windows-msvc-vcpkg-headless",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"exclude": {
|
||||
"label": "gpu|slow|platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-gpu",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "gpu"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fuzz",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "fuzz"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stress",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "stress"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.28010.2026
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PanoPainter", "PanoPainter.vcxproj", "{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}"
|
||||
EndProject
|
||||
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PanoPainterPackage", "PanoPainterPackage\PanoPainterPackage.wapproj", "{3A716FB6-DE62-439F-83B6-3C40915D6678}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|Any CPU.ActiveCfg = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Build.0 = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.Build.0 = Debug|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|Any CPU.ActiveCfg = Release|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.ActiveCfg = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Build.0 = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Deploy.0 = Release|x64
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.ActiveCfg = Release|Win32
|
||||
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.Build.0 = Release|Win32
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Build.0 = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Build.0 = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Deploy.0 = Debug|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.ActiveCfg = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Build.0 = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Deploy.0 = Release|x64
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.ActiveCfg = Release|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Build.0 = Release|x86
|
||||
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Deploy.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3E8EFC4B-CEA1-4408-8628-7D2C0F6C43C8}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,634 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>PanoPainter</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
||||
<ProjectName>PanoPainter</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v141</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>
|
||||
</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;DEBUG;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>python .\scripts\pre-build.py debug</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>
|
||||
</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<ExceptionHandling>false</ExceptionHandling>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>python .\scripts\pre-build.py release</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="libs\fmt\src\format.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\nanort\nanort.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
|
||||
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
|
||||
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\abr.cpp" />
|
||||
<ClCompile Include="src\action.cpp" />
|
||||
<ClCompile Include="src\app.cpp" />
|
||||
<ClCompile Include="src\app_cloud.cpp" />
|
||||
<ClCompile Include="src\app_commands.cpp" />
|
||||
<ClCompile Include="src\app_dialogs.cpp" />
|
||||
<ClCompile Include="src\app_events.cpp" />
|
||||
<ClCompile Include="src\app_layout.cpp" />
|
||||
<ClCompile Include="src\app_shaders.cpp" />
|
||||
<ClCompile Include="src\app_vr.cpp" />
|
||||
<ClCompile Include="src\asset.cpp" />
|
||||
<ClCompile Include="src\bezier.cpp" />
|
||||
<ClCompile Include="src\binary_stream.cpp" />
|
||||
<ClCompile Include="src\brush.cpp" />
|
||||
<ClCompile Include="src\canvas.cpp" />
|
||||
<ClCompile Include="src\canvas_actions.cpp" />
|
||||
<ClCompile Include="src\canvas_layer.cpp" />
|
||||
<ClCompile Include="src\canvas_modes.cpp" />
|
||||
<ClCompile Include="src\event.cpp" />
|
||||
<ClCompile Include="src\font.cpp" />
|
||||
<ClCompile Include="src\hmd.cpp" />
|
||||
<ClCompile Include="src\image.cpp" />
|
||||
<ClCompile Include="src\layout.cpp" />
|
||||
<ClCompile Include="src\log.cpp" />
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\mp4enc.cpp" />
|
||||
<ClCompile Include="src\node.cpp" />
|
||||
<ClCompile Include="src\node_about.cpp" />
|
||||
<ClCompile Include="src\node_border.cpp" />
|
||||
<ClCompile Include="src\node_button.cpp" />
|
||||
<ClCompile Include="src\node_button_custom.cpp" />
|
||||
<ClCompile Include="src\node_canvas.cpp" />
|
||||
<ClCompile Include="src\node_changelog.cpp" />
|
||||
<ClCompile Include="src\node_checkbox.cpp" />
|
||||
<ClCompile Include="src\node_colorwheel.cpp" />
|
||||
<ClCompile Include="src\node_color_quad.cpp" />
|
||||
<ClCompile Include="src\node_combobox.cpp" />
|
||||
<ClCompile Include="src\node_dialog_browse.cpp" />
|
||||
<ClCompile Include="src\node_dialog_cloud.cpp" />
|
||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp" />
|
||||
<ClCompile Include="src\node_dialog_layer_rename.cpp" />
|
||||
<ClCompile Include="src\node_dialog_open.cpp" />
|
||||
<ClCompile Include="src\node_dialog_picker.cpp" />
|
||||
<ClCompile Include="src\node_dialog_resize.cpp" />
|
||||
<ClCompile Include="src\node_icon.cpp" />
|
||||
<ClCompile Include="src\node_image.cpp" />
|
||||
<ClCompile Include="src\node_image_texture.cpp" />
|
||||
<ClCompile Include="src\node_input_box.cpp" />
|
||||
<ClCompile Include="src\node_message_box.cpp" />
|
||||
<ClCompile Include="src\node_metadata.cpp" />
|
||||
<ClCompile Include="src\node_panel_brush.cpp" />
|
||||
<ClCompile Include="src\node_panel_color.cpp" />
|
||||
<ClCompile Include="src\node_panel_floating.cpp" />
|
||||
<ClCompile Include="src\node_panel_grid.cpp" />
|
||||
<ClCompile Include="src\node_panel_layer.cpp" />
|
||||
<ClCompile Include="src\node_panel_quick.cpp" />
|
||||
<ClCompile Include="src\node_panel_stroke.cpp" />
|
||||
<ClCompile Include="src\node_panel_animation.cpp" />
|
||||
<ClCompile Include="src\node_popup_menu.cpp" />
|
||||
<ClCompile Include="src\node_progress_bar.cpp" />
|
||||
<ClCompile Include="src\node_remote_page.cpp" />
|
||||
<ClCompile Include="src\node_scroll.cpp" />
|
||||
<ClCompile Include="src\node_settings.cpp" />
|
||||
<ClCompile Include="src\node_shorcuts.cpp" />
|
||||
<ClCompile Include="src\node_slider.cpp" />
|
||||
<ClCompile Include="src\node_stroke_preview.cpp" />
|
||||
<ClCompile Include="src\node_text.cpp" />
|
||||
<ClCompile Include="src\node_text_input.cpp" />
|
||||
<ClCompile Include="src\node_tool_bucket.cpp" />
|
||||
<ClCompile Include="src\node_usermanual.cpp" />
|
||||
<ClCompile Include="src\node_viewport.cpp" />
|
||||
<ClCompile Include="src\pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\rtt.cpp" />
|
||||
<ClCompile Include="src\serializer.cpp" />
|
||||
<ClCompile Include="src\settings.cpp" />
|
||||
<ClCompile Include="src\shader.cpp" />
|
||||
<ClCompile Include="src\shape.cpp" />
|
||||
<ClCompile Include="src\texture.cpp" />
|
||||
<ClCompile Include="src\util.cpp" />
|
||||
<ClCompile Include="src\version.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\wacom.cpp" />
|
||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="libs\hash-library\md5.h" />
|
||||
<ClInclude Include="libs\nanort\nanort.h" />
|
||||
<ClInclude Include="libs\sqlite3\sqlite3.h" />
|
||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="src\abr.h" />
|
||||
<ClInclude Include="src\action.h" />
|
||||
<ClInclude Include="src\app.h" />
|
||||
<ClInclude Include="src\asset.h" />
|
||||
<ClInclude Include="src\bezier.h" />
|
||||
<ClInclude Include="src\binary_stream.h" />
|
||||
<ClInclude Include="src\brush.h" />
|
||||
<ClInclude Include="src\canvas.h" />
|
||||
<ClInclude Include="src\canvas_actions.h" />
|
||||
<ClInclude Include="src\canvas_layer.h" />
|
||||
<ClInclude Include="src\canvas_modes.h" />
|
||||
<ClInclude Include="src\event.h" />
|
||||
<ClInclude Include="src\font.h" />
|
||||
<ClInclude Include="src\hmd.h" />
|
||||
<ClInclude Include="src\image.h" />
|
||||
<ClInclude Include="src\keymap.h" />
|
||||
<ClInclude Include="src\layout.h" />
|
||||
<ClInclude Include="src\log.h" />
|
||||
<ClInclude Include="src\mp4enc.h" />
|
||||
<ClInclude Include="src\node.h" />
|
||||
<ClInclude Include="src\node_about.h" />
|
||||
<ClInclude Include="src\node_border.h" />
|
||||
<ClInclude Include="src\node_button.h" />
|
||||
<ClInclude Include="src\node_button_custom.h" />
|
||||
<ClInclude Include="src\node_canvas.h" />
|
||||
<ClInclude Include="src\node_changelog.h" />
|
||||
<ClInclude Include="src\node_checkbox.h" />
|
||||
<ClInclude Include="src\node_colorwheel.h" />
|
||||
<ClInclude Include="src\node_color_quad.h" />
|
||||
<ClInclude Include="src\node_combobox.h" />
|
||||
<ClInclude Include="src\node_dialog_browse.h" />
|
||||
<ClInclude Include="src\node_dialog_cloud.h" />
|
||||
<ClInclude Include="src\node_dialog_export_ppbr.h" />
|
||||
<ClInclude Include="src\node_dialog_layer_rename.h" />
|
||||
<ClInclude Include="src\node_dialog_open.h" />
|
||||
<ClInclude Include="src\node_dialog_picker.h" />
|
||||
<ClInclude Include="src\node_dialog_resize.h" />
|
||||
<ClInclude Include="src\node_icon.h" />
|
||||
<ClInclude Include="src\node_image.h" />
|
||||
<ClInclude Include="src\node_image_texture.h" />
|
||||
<ClInclude Include="src\node_input_box.h" />
|
||||
<ClInclude Include="src\node_message_box.h" />
|
||||
<ClInclude Include="src\node_metadata.h" />
|
||||
<ClInclude Include="src\node_panel_brush.h" />
|
||||
<ClInclude Include="src\node_panel_color.h" />
|
||||
<ClInclude Include="src\node_panel_floating.h" />
|
||||
<ClInclude Include="src\node_panel_grid.h" />
|
||||
<ClInclude Include="src\node_panel_layer.h" />
|
||||
<ClInclude Include="src\node_panel_quick.h" />
|
||||
<ClInclude Include="src\node_panel_stroke.h" />
|
||||
<ClInclude Include="src\node_panel_animation.h" />
|
||||
<ClInclude Include="src\node_popup_menu.h" />
|
||||
<ClInclude Include="src\node_progress_bar.h" />
|
||||
<ClInclude Include="src\node_remote_page.h" />
|
||||
<ClInclude Include="src\node_scroll.h" />
|
||||
<ClInclude Include="src\node_settings.h" />
|
||||
<ClInclude Include="src\node_shorcuts.h" />
|
||||
<ClInclude Include="src\node_slider.h" />
|
||||
<ClInclude Include="src\node_stroke_preview.h" />
|
||||
<ClInclude Include="src\node_text.h" />
|
||||
<ClInclude Include="src\node_text_input.h" />
|
||||
<ClInclude Include="src\node_tool_bucket.h" />
|
||||
<ClInclude Include="src\node_usermanual.h" />
|
||||
<ClInclude Include="src\node_viewport.h" />
|
||||
<ClInclude Include="src\pch.h" />
|
||||
<ClInclude Include="src\rtt.h" />
|
||||
<ClInclude Include="src\serializer.h" />
|
||||
<ClInclude Include="src\settings.h" />
|
||||
<ClInclude Include="src\shader.h" />
|
||||
<ClInclude Include="src\shape.h" />
|
||||
<ClInclude Include="src\texture.h" />
|
||||
<ClInclude Include="src\util.h" />
|
||||
<ClInclude Include="src\version.gen.h" />
|
||||
<ClInclude Include="src\version.h" />
|
||||
<ClInclude Include="src\wacom.h" />
|
||||
<ClInclude Include="libs\jpeg\jpgd.h" />
|
||||
<ClInclude Include="libs\jpeg\jpge.h" />
|
||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h" />
|
||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H" />
|
||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H" />
|
||||
<ClInclude Include="libs\wacom\WinTab\Utils.h" />
|
||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PanoPainter.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="libs\glm\util\glm.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xml Include="data\dialogs\about.xml" />
|
||||
<Xml Include="data\dialogs\brush-export.xml" />
|
||||
<Xml Include="data\dialogs\changelog.xml" />
|
||||
<Xml Include="data\dialogs\cloud-browse.xml" />
|
||||
<Xml Include="data\dialogs\color-picker.xml" />
|
||||
<Xml Include="data\dialogs\doc-browse.xml" />
|
||||
<Xml Include="data\dialogs\doc-new.xml" />
|
||||
<Xml Include="data\dialogs\doc-open.xml" />
|
||||
<Xml Include="data\dialogs\doc-resize.xml" />
|
||||
<Xml Include="data\dialogs\doc-save.xml" />
|
||||
<Xml Include="data\dialogs\input-box.xml" />
|
||||
<Xml Include="data\dialogs\layer-rename.xml" />
|
||||
<Xml Include="data\dialogs\message-box.xml" />
|
||||
<Xml Include="data\dialogs\panel-animation.xml" />
|
||||
<Xml Include="data\dialogs\panel-floating.xml" />
|
||||
<Xml Include="data\dialogs\panel-grid.xml" />
|
||||
<Xml Include="data\dialogs\panel-layers.xml" />
|
||||
<Xml Include="data\dialogs\panel-brushes.xml" />
|
||||
<Xml Include="data\dialogs\panel-presets.xml" />
|
||||
<Xml Include="data\dialogs\panel-quick.xml" />
|
||||
<Xml Include="data\dialogs\panel-stroke.xml" />
|
||||
<Xml Include="data\dialogs\progress-bar.xml" />
|
||||
<Xml Include="data\dialogs\remote-page.xml" />
|
||||
<Xml Include="data\dialogs\settings.xml" />
|
||||
<Xml Include="data\dialogs\shortcuts.xml" />
|
||||
<Xml Include="data\dialogs\usermanual.xml" />
|
||||
<Xml Include="data\layout.xml">
|
||||
<SubType>Designer</SubType>
|
||||
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
</DeploymentContent>
|
||||
</Xml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="data\shaders\atlas.glsl" />
|
||||
<None Include="data\shaders\bake-uv.glsl" />
|
||||
<None Include="data\shaders\checkerboard.glsl" />
|
||||
<None Include="data\shaders\color-hue.glsl" />
|
||||
<None Include="data\shaders\color-quad.glsl" />
|
||||
<None Include="data\shaders\color-tri.glsl" />
|
||||
<None Include="data\shaders\color.glsl" />
|
||||
<None Include="data\shaders\comp-draw.glsl" />
|
||||
<None Include="data\shaders\comp-erase.glsl" />
|
||||
<None Include="data\shaders\equirect.glsl" />
|
||||
<None Include="data\shaders\font.glsl" />
|
||||
<None Include="data\shaders\include\blend-stroke.glsl" />
|
||||
<None Include="data\shaders\include\blend.glsl" />
|
||||
<None Include="data\shaders\include\blur.glsl" />
|
||||
<None Include="data\shaders\include\color.glsl" />
|
||||
<None Include="data\shaders\include\ext-fb-fetch.glsl" />
|
||||
<None Include="data\shaders\include\hsv.glsl" />
|
||||
<None Include="data\shaders\include\rand.glsl" />
|
||||
<None Include="data\shaders\lambert.glsl" />
|
||||
<None Include="data\shaders\lightmap.glsl" />
|
||||
<None Include="data\shaders\stroke-dilate.glsl" />
|
||||
<None Include="data\shaders\stroke-instanced.glsl" />
|
||||
<None Include="data\shaders\stroke-pad.glsl" />
|
||||
<None Include="data\shaders\stroke-preview.glsl" />
|
||||
<None Include="data\shaders\stroke.glsl" />
|
||||
<None Include="data\shaders\texture-alpha.glsl" />
|
||||
<None Include="data\shaders\texture-blend.glsl" />
|
||||
<None Include="data\shaders\texture-colorize.glsl" />
|
||||
<None Include="data\shaders\texture-mask.glsl" />
|
||||
<None Include="data\shaders\texture.glsl" />
|
||||
<None Include="data\shaders\uvs.glsl" />
|
||||
<None Include="data\shaders\vertex-color.glsl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xsd Include="extra\layout.xsd">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||
</Xsd>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,854 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\ui">
|
||||
<UniqueIdentifier>{600b8daa-4234-4c37-b4ba-c22cad7d1dc3}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs">
|
||||
<UniqueIdentifier>{6d64b115-02d1-43e0-86c8-c8212f51162d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\jpeg">
|
||||
<UniqueIdentifier>{dc178d53-6a6d-4a18-a93c-d4994340515f}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\WinTab">
|
||||
<UniqueIdentifier>{54dc9f46-d2e0-466c-90d2-eb5d72d5799d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\yoga">
|
||||
<UniqueIdentifier>{a4a12057-835e-47ff-be4d-ce58b36cecf5}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\tinyxml2">
|
||||
<UniqueIdentifier>{6fe315aa-e2b9-4f01-8291-683a5fda123b}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\poly2tri">
|
||||
<UniqueIdentifier>{bda6fa93-a186-41ca-9bd9-49b7e0fd1ca4}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="extras">
|
||||
<UniqueIdentifier>{e631ac80-1b9b-424f-8adf-e2bab71a566d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\sqlite3">
|
||||
<UniqueIdentifier>{ef44d179-f28b-458c-b3df-be2895553149}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\nanort">
|
||||
<UniqueIdentifier>{be0c0053-abd8-4e2d-a294-7c54511b05a6}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\hash">
|
||||
<UniqueIdentifier>{2a784067-6741-47a3-b668-cc45f2224286}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\fmt">
|
||||
<UniqueIdentifier>{7b4f5b47-7a8b-4e4c-9e82-399bb5047ffc}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="shaders">
|
||||
<UniqueIdentifier>{b55fb692-a845-4ef2-9b0e-5b2dd8bd125f}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="shaders\include">
|
||||
<UniqueIdentifier>{a2cacb13-2854-44ee-9511-6cb8ac587428}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="libs\glad">
|
||||
<UniqueIdentifier>{ca37521b-213f-4bcf-acfd-eda1483a30b2}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="extras\dialogs">
|
||||
<UniqueIdentifier>{5ecb54ed-7c3d-46fd-9b5d-227abdbc5954}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\app.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\image.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\shader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\shape.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\texture.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\font.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\util.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\asset.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\rtt.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\bezier.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\brush.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\log.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\action.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\event.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_modes.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_border.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_button.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_button_custom.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_canvas.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_checkbox.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_color_quad.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_open.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_icon.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_image.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_image_texture.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_message_box.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_brush.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_color.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_layer.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_stroke.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_popup_menu.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_settings.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_slider.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_stroke_preview.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_text.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_text_input.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_viewport.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\layout.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_scroll.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_shaders.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_layout.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_events.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpgd.cpp">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\jpeg\jpge.cpp">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
|
||||
<Filter>libs\tinyxml2</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\wacom.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_layer_rename.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_dialogs.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
|
||||
<Filter>libs\poly2tri</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_progress_bar.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_browse.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_commands.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_cloud.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_cloud.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_combobox.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_picker.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_colorwheel.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_grid.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\version.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_about.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_changelog.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_usermanual.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_resize.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\sqlite3\sqlite3.c">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\hmd.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\app_vr.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\nanort\nanort.cc">
|
||||
<Filter>libs\nanort</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\hash-library\md5.cpp">
|
||||
<Filter>libs\hash</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\abr.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\format.cc">
|
||||
<Filter>libs\fmt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\fmt\src\posix.cc">
|
||||
<Filter>libs\fmt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_quick.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\binary_stream.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\serializer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\log.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_floating.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\settings.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_actions.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\canvas_layer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_tool_bucket.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad.c">
|
||||
<Filter>libs\glad</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\glad\src\glad_wgl.c">
|
||||
<Filter>libs\glad</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_dialog_export_ppbr.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_input_box.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_panel_animation.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
|
||||
<Filter>libs\yoga</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\mp4enc.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_remote_page.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_metadata.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\node_shorcuts.cpp">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="libs\jpeg\jpgd.h">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\jpeg\jpge.h">
|
||||
<Filter>libs\jpeg</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\tinyxml2\tinyxml2.h">
|
||||
<Filter>libs\tinyxml2</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\Utils.h">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\WINTAB.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H">
|
||||
<Filter>libs\WinTab</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\sqlite3\sqlite3.h">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\sqlite3\sqlite3ext.h">
|
||||
<Filter>libs\sqlite3</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\nanort\nanort.h">
|
||||
<Filter>libs\nanort</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="libs\hash-library\md5.h">
|
||||
<Filter>libs\hash</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\abr.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\action.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\app.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\asset.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\bezier.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\binary_stream.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\brush.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_actions.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_layer.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\canvas_modes.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\event.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\font.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\hmd.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\image.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\keymap.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\log.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\pch.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\rtt.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\serializer.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\settings.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\shader.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\shape.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\texture.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\util.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\version.gen.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\version.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\wacom.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\layout.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_about.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_border.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_button.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_button_custom.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_canvas.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_changelog.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_checkbox.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_color_quad.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_colorwheel.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_combobox.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_browse.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_cloud.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_export_ppbr.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_layer_rename.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_open.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_picker.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_dialog_resize.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_icon.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_image.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_image_texture.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_input_box.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_message_box.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_brush.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_color.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_floating.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_grid.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_layer.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_quick.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_stroke.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_popup_menu.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_progress_bar.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_scroll.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_settings.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_slider.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_stroke_preview.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_text.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_text_input.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_tool_bucket.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_usermanual.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_viewport.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_panel_animation.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_remote_page.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_metadata.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\node_shorcuts.h">
|
||||
<Filter>Source Files\ui</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\mp4enc.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PanoPainter.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="libs\glm\util\glm.natvis">
|
||||
<Filter>extras</Filter>
|
||||
</Natvis>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xml Include="data\layout.xml">
|
||||
<Filter>extras</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\changelog.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\about.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\usermanual.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\brush-export.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-layers.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-brushes.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-stroke.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-grid.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-quick.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\color-picker.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\input-box.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\message-box.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\progress-bar.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\layer-rename.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-resize.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-browse.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-new.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-save.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\cloud-browse.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\settings.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\doc-open.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-floating.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-presets.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\panel-animation.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\remote-page.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
<Xml Include="data\dialogs\shortcuts.xml">
|
||||
<Filter>extras\dialogs</Filter>
|
||||
</Xml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="data\shaders\texture.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blend-stroke.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blur.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\color.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\ext-fb-fetch.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\hsv.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\rand.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\include\blend.glsl">
|
||||
<Filter>shaders\include</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\comp-draw.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\comp-erase.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\equirect.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\font.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\lambert.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\lightmap.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-instanced.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-preview.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-alpha.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-blend.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\uvs.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\vertex-color.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\atlas.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\bake-uv.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\checkerboard.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-hue.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-quad.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\color-tri.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-colorize.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\texture-mask.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-dilate.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
<None Include="data\shaders\stroke-pad.glsl">
|
||||
<Filter>shaders</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="icon.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Xsd Include="extra\layout.xsd">
|
||||
<Filter>extras</Filter>
|
||||
</Xsd>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterAndroidNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
link_directories(
|
||||
../../libs/curl-android-ios/android/${ANDROID_ABI}
|
||||
@@ -23,11 +28,68 @@ add_library(yuv SHARED IMPORTED)
|
||||
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/../../libs/libyuv/lib/android/${ANDROID_ABI}/libyuv.so)
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
@@ -128,6 +190,9 @@ add_library(
|
||||
../../src/node_metadata.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
src/main/cpp
|
||||
../src/cpp
|
||||
|
||||
20
android/cmake/PanoPainterAndroidVendorPatches.cmake
Normal file
20
android/cmake/PanoPainterAndroidVendorPatches.cmake
Normal file
@@ -0,0 +1,20 @@
|
||||
set(PP_ANDROID_VENDOR_PATCH_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
|
||||
function(pp_apply_android_nanort_patch)
|
||||
set(nanort_header "${PP_ANDROID_VENDOR_PATCH_DIR}/../../libs/nanort/nanort.h")
|
||||
file(READ "${nanort_header}" nanort_contents)
|
||||
|
||||
set(nanort_before " const size_t vertex_stride_bytes_;")
|
||||
set(nanort_after " size_t vertex_stride_bytes_;")
|
||||
|
||||
if(nanort_contents MATCHES "${nanort_before}")
|
||||
string(REPLACE
|
||||
"${nanort_before}"
|
||||
"${nanort_after}"
|
||||
nanort_contents
|
||||
"${nanort_contents}")
|
||||
file(WRITE "${nanort_header}" "${nanort_contents}")
|
||||
elseif(NOT nanort_contents MATCHES "${nanort_after}")
|
||||
message(FATAL_ERROR "Unexpected nanort.h layout; Android nanort patch could not be applied")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterFocusNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
# build native_app_glue as a static lib
|
||||
add_library(
|
||||
@@ -17,23 +22,81 @@ set_target_properties(
|
||||
${CMAKE_SOURCE_DIR}/../../libs/wave_sdk/wvr_client/lib/${ANDROID_ABI}/libwvr_api.so
|
||||
)
|
||||
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
# Specifies a library name, specifies whether the library is STATIC or
|
||||
# SHARED, and provides relative paths to the source code. You can
|
||||
# define multiple libraries by adding multiple add.library() commands,
|
||||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
../../libs/yoga/yoga/Utils.cpp
|
||||
../../libs/yoga/yoga/YGConfig.cpp
|
||||
../../libs/yoga/yoga/YGEnums.cpp
|
||||
../../libs/yoga/yoga/YGLayout.cpp
|
||||
../../libs/yoga/yoga/YGMarker.cpp
|
||||
../../libs/yoga/yoga/YGNode.cpp
|
||||
../../libs/yoga/yoga/YGNodePrint.cpp
|
||||
../../libs/yoga/yoga/YGStyle.cpp
|
||||
@@ -121,6 +184,9 @@ add_library(
|
||||
../../src/settings.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
../../libs/wave_sdk/wvr_client/include
|
||||
src/main/cpp
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterQuestNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
# build native_app_glue as a static lib
|
||||
add_library(
|
||||
@@ -25,23 +30,81 @@ set_target_properties(
|
||||
)
|
||||
|
||||
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
# Specifies a library name, specifies whether the library is STATIC or
|
||||
# SHARED, and provides relative paths to the source code. You can
|
||||
# define multiple libraries by adding multiple add.library() commands,
|
||||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
../../libs/yoga/yoga/Utils.cpp
|
||||
../../libs/yoga/yoga/YGConfig.cpp
|
||||
../../libs/yoga/yoga/YGEnums.cpp
|
||||
../../libs/yoga/yoga/YGLayout.cpp
|
||||
../../libs/yoga/yoga/YGMarker.cpp
|
||||
../../libs/yoga/yoga/YGNode.cpp
|
||||
../../libs/yoga/yoga/YGNodePrint.cpp
|
||||
../../libs/yoga/yoga/YGStyle.cpp
|
||||
@@ -129,6 +192,9 @@ add_library(
|
||||
../../src/settings.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
../../libs/ovr_mobile/include
|
||||
../../libs/ovr_platform/Include
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
#include "keymap.h"
|
||||
#include "main.h"
|
||||
#include "settings.h"
|
||||
#if __has_include("com_omixlab_panopainter_MainActivity.h")
|
||||
#include "com_omixlab_panopainter_MainActivity.h"
|
||||
#endif
|
||||
|
||||
#ifdef __QUEST__
|
||||
#include "oculus_vr.h"
|
||||
@@ -241,7 +243,7 @@ extern "C"
|
||||
#ifdef __FOCUS__
|
||||
JNIEXPORT void JNICALL Java_com_omixlab_panopainter_MainActivity_init_vr(JNIEnv *env, jobject activity, jobject am)
|
||||
{
|
||||
Asset::m_am = AAssetManager_fromJava(env, am);
|
||||
Asset::set_android_asset_manager(AAssetManager_fromJava(env, am));
|
||||
wave_init(0, 0, 0);
|
||||
}
|
||||
#endif
|
||||
@@ -700,7 +702,7 @@ static int engine_init_display(struct engine* engine) {
|
||||
LOG("PROP Maker: %s", os_props["ro.product.manufacturer"].c_str());
|
||||
LOG("PROP Mode: %s", os_props["ro.product.model"].c_str());
|
||||
|
||||
Asset::m_am = engine->app->activity->assetManager;
|
||||
Asset::set_android_asset_manager(engine->app->activity->assetManager);
|
||||
App::I->and_app = engine->app;
|
||||
App::I->and_engine = engine;
|
||||
|
||||
|
||||
23
cmake/PanoPainterAutomation.cmake
Normal file
23
cmake/PanoPainterAutomation.cmake
Normal file
@@ -0,0 +1,23 @@
|
||||
find_program(PP_POWERSHELL_COMMAND NAMES pwsh powershell)
|
||||
|
||||
function(pp_add_powershell_automation_target target_name)
|
||||
cmake_parse_arguments(PP_AUTOMATION_TARGET "" "COMMENT" "ARGUMENTS" ${ARGN})
|
||||
|
||||
if(PP_POWERSHELL_COMMAND)
|
||||
add_custom_target(${target_name}
|
||||
COMMAND "${PP_POWERSHELL_COMMAND}"
|
||||
-NoProfile
|
||||
-ExecutionPolicy Bypass
|
||||
${PP_AUTOMATION_TARGET_ARGUMENTS}
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
COMMENT "${PP_AUTOMATION_TARGET_COMMENT}"
|
||||
USES_TERMINAL
|
||||
VERBATIM)
|
||||
else()
|
||||
add_custom_target(${target_name}
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "PowerShell was not found; cannot run ${target_name}."
|
||||
COMMAND "${CMAKE_COMMAND}" -E false
|
||||
COMMENT "${PP_AUTOMATION_TARGET_COMMENT}"
|
||||
VERBATIM)
|
||||
endif()
|
||||
endfunction()
|
||||
20
cmake/PanoPainterOptions.cmake
Normal file
20
cmake/PanoPainterOptions.cmake
Normal file
@@ -0,0 +1,20 @@
|
||||
option(PP_BUILD_APP "Build the PanoPainter application target from root CMake." ON)
|
||||
option(PP_BUILD_TESTS "Build PanoPainter tests." ON)
|
||||
option(PP_BUILD_TOOLS "Build PanoPainter automation tools." ON)
|
||||
|
||||
option(PP_ENABLE_OPENGL "Enable the OpenGL renderer backend." ON)
|
||||
option(PP_ENABLE_VULKAN_EXPERIMENTAL "Enable non-production Vulkan experiments." OFF)
|
||||
option(PP_ENABLE_VR "Enable VR support." ON)
|
||||
option(PP_ENABLE_CLOUD "Enable cloud/network features." ON)
|
||||
option(PP_ENABLE_VIDEO "Enable MP4/timelapse video features." ON)
|
||||
|
||||
option(PP_ENABLE_ASAN "Enable AddressSanitizer where supported." OFF)
|
||||
option(PP_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer where supported." OFF)
|
||||
option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where supported." OFF)
|
||||
option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF)
|
||||
option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF)
|
||||
option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF)
|
||||
option(PP_USE_VCPKG_TINYXML2 "Use the vcpkg tinyxml2 package for component targets." OFF)
|
||||
|
||||
set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.")
|
||||
set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus)
|
||||
44
cmake/PanoPainterPackageTargets.cmake
Normal file
44
cmake/PanoPainterPackageTargets.cmake
Normal file
@@ -0,0 +1,44 @@
|
||||
include(PanoPainterAutomation)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_package_readiness
|
||||
COMMENT "Report package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_windows_app_package_smoke
|
||||
COMMENT "Build the Windows app artifact and report package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-Preset windows-msvc-default
|
||||
-Configuration Debug
|
||||
-Target PanoPainter
|
||||
-CMakeCommand "${CMAKE_COMMAND}")
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_standard_native_package
|
||||
COMMENT "Build retained Android standard native package library."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
|
||||
-Packages standard)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_vr_native_package_configure
|
||||
COMMENT "Configure retained Android Quest/Focus native package paths."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
|
||||
-Packages quest,focus
|
||||
-ConfigureOnly)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_native_package_smoke
|
||||
COMMENT "Run retained Android native package checks through package-smoke."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-AndroidNativeChecks
|
||||
-PackageKinds android-standard-apk,android-quest-apk,android-focus-apk)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_linux_webgl_package_readiness
|
||||
COMMENT "Report retained Linux and WebGL package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds linux-app,webgl)
|
||||
26
cmake/PanoPainterPlatformTargets.cmake
Normal file
26
cmake/PanoPainterPlatformTargets.cmake
Normal file
@@ -0,0 +1,26 @@
|
||||
include(PanoPainterAutomation)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_headless
|
||||
COMMENT "Run the default headless platform build matrix."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1")
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_android_assets
|
||||
COMMENT "Build Android root CMake asset component across standard/VR presets."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1"
|
||||
-Presets android-arm64,android-x64,android-quest-arm64,android-focus-arm64
|
||||
-Targets pp_assets)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_vcpkg_ui_core
|
||||
COMMENT "Build the Windows vcpkg-backed UI core dependency boundary."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1"
|
||||
-Presets windows-msvc-vcpkg-headless
|
||||
-Targets pp_ui_core,pp_ui_core_layout_xml_tests)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_apple_remote
|
||||
COMMENT "Run remote Apple compile validation for macOS, iOS simulator, and iOS device."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/apple-remote-build.ps1"
|
||||
-Presets macos,ios-simulator,ios-device)
|
||||
29
cmake/PanoPainterRuntime.cmake
Normal file
29
cmake/PanoPainterRuntime.cmake
Normal file
@@ -0,0 +1,29 @@
|
||||
function(pp_configure_windows_runtime_payloads target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "pp_configure_windows_runtime_payloads target does not exist: ${target_name}")
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET "${target_name}" POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/data"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/BugTrapU-x64.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/libyuv.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/libmp4v2.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/openh264-2.0.0-win64.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/bin/win64/openvr_api.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/openvr_api.dll"
|
||||
VERBATIM)
|
||||
endfunction()
|
||||
214
cmake/PanoPainterSources.cmake
Normal file
214
cmake/PanoPainterSources.cmake
Normal file
@@ -0,0 +1,214 @@
|
||||
set(PP_LEGACY_ENGINE_SOURCES
|
||||
src/hmd.cpp
|
||||
src/log.cpp
|
||||
src/mp4enc.cpp
|
||||
src/util.cpp
|
||||
src/wacom.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_ASSETS_IO_SOURCES
|
||||
src/abr.cpp
|
||||
src/asset.cpp
|
||||
src/binary_stream.cpp
|
||||
src/image.cpp
|
||||
src/serializer.cpp
|
||||
src/settings.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
|
||||
src/action.cpp
|
||||
src/bezier.cpp
|
||||
src/brush.cpp
|
||||
src/canvas.cpp
|
||||
src/canvas_actions.cpp
|
||||
src/canvas_layer.cpp
|
||||
src/event.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_RENDERER_GL_SOURCES
|
||||
src/font.cpp
|
||||
src/rtt.cpp
|
||||
src/shader.cpp
|
||||
src/shape.cpp
|
||||
src/texture.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_UI_CORE_SOURCES
|
||||
src/layout.cpp
|
||||
src/node.cpp
|
||||
src/node_border.cpp
|
||||
src/node_button.cpp
|
||||
src/node_button_custom.cpp
|
||||
src/node_checkbox.cpp
|
||||
src/node_combobox.cpp
|
||||
src/node_icon.cpp
|
||||
src/node_image.cpp
|
||||
src/node_image_texture.cpp
|
||||
src/node_input_box.cpp
|
||||
src/node_popup_menu.cpp
|
||||
src/node_progress_bar.cpp
|
||||
src/node_remote_page.cpp
|
||||
src/node_scroll.cpp
|
||||
src/node_settings.cpp
|
||||
src/node_shorcuts.cpp
|
||||
src/node_slider.cpp
|
||||
src/node_text.cpp
|
||||
src/node_text_input.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
src/canvas_modes.cpp
|
||||
src/legacy_app_shell_services.cpp
|
||||
src/legacy_app_shell_services.h
|
||||
src/legacy_canvas_tool_services.cpp
|
||||
src/legacy_canvas_tool_services.h
|
||||
src/legacy_canvas_view_services.cpp
|
||||
src/legacy_canvas_view_services.h
|
||||
src/legacy_preference_storage.cpp
|
||||
src/legacy_preference_storage.h
|
||||
src/legacy_document_canvas_services.cpp
|
||||
src/legacy_document_canvas_services.h
|
||||
src/legacy_document_layer_services.cpp
|
||||
src/legacy_document_layer_services.h
|
||||
src/legacy_history_services.cpp
|
||||
src/legacy_history_services.h
|
||||
src/legacy_recording_services.cpp
|
||||
src/legacy_recording_services.h
|
||||
src/legacy_ui_overlay_services.cpp
|
||||
src/legacy_ui_overlay_services.h
|
||||
src/pch.cpp
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_cloud.cpp
|
||||
src/app_commands.cpp
|
||||
src/app_dialogs.cpp
|
||||
src/app_events.cpp
|
||||
src/app_layout.cpp
|
||||
src/app_shaders.cpp
|
||||
src/app_vr.cpp
|
||||
src/legacy_app_dialog_services.cpp
|
||||
src/legacy_app_dialog_services.h
|
||||
src/legacy_app_preference_services.cpp
|
||||
src/legacy_app_preference_services.h
|
||||
src/legacy_app_startup_services.cpp
|
||||
src/legacy_app_startup_services.h
|
||||
src/legacy_brush_package_import_services.cpp
|
||||
src/legacy_brush_package_import_services.h
|
||||
src/legacy_brush_package_export_services.cpp
|
||||
src/legacy_brush_package_export_services.h
|
||||
src/legacy_cloud_services.cpp
|
||||
src/legacy_cloud_services.h
|
||||
src/legacy_document_export_services.cpp
|
||||
src/legacy_document_export_services.h
|
||||
src/legacy_document_open_services.cpp
|
||||
src/legacy_document_open_services.h
|
||||
src/legacy_document_session_services.cpp
|
||||
src/legacy_document_session_services.h
|
||||
src/platform_legacy/legacy_platform_services.cpp
|
||||
src/platform_legacy/legacy_platform_services.h
|
||||
src/version.cpp
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_UI_SOURCES
|
||||
src/legacy_brush_ui_services.cpp
|
||||
src/legacy_brush_ui_services.h
|
||||
src/legacy_document_animation_services.cpp
|
||||
src/legacy_document_animation_services.h
|
||||
src/legacy_grid_ui_services.cpp
|
||||
src/legacy_grid_ui_services.h
|
||||
src/legacy_quick_ui_services.cpp
|
||||
src/legacy_quick_ui_services.h
|
||||
src/node_about.cpp
|
||||
src/node_canvas.cpp
|
||||
src/node_changelog.cpp
|
||||
src/node_color_quad.cpp
|
||||
src/node_colorwheel.cpp
|
||||
src/node_dialog_browse.cpp
|
||||
src/node_dialog_cloud.cpp
|
||||
src/node_dialog_export_ppbr.cpp
|
||||
src/node_dialog_layer_rename.cpp
|
||||
src/node_dialog_open.cpp
|
||||
src/node_dialog_picker.cpp
|
||||
src/node_dialog_resize.cpp
|
||||
src/node_message_box.cpp
|
||||
src/node_metadata.cpp
|
||||
src/node_panel_animation.cpp
|
||||
src/node_panel_brush.cpp
|
||||
src/node_panel_color.cpp
|
||||
src/node_panel_floating.cpp
|
||||
src/node_panel_grid.cpp
|
||||
src/node_panel_layer.cpp
|
||||
src/node_panel_quick.cpp
|
||||
src/node_panel_stroke.cpp
|
||||
src/node_stroke_preview.cpp
|
||||
src/node_tool_bucket.cpp
|
||||
src/node_usermanual.cpp
|
||||
src/node_viewport.cpp
|
||||
)
|
||||
|
||||
set(PP_WINDOWS_PLATFORM_SOURCES
|
||||
src/main.cpp
|
||||
src/platform_windows/windows_platform_services.cpp
|
||||
src/platform_windows/windows_platform_services.h
|
||||
)
|
||||
|
||||
set(PP_WINDOWS_APP_SOURCES
|
||||
PanoPainter.rc
|
||||
)
|
||||
|
||||
set(PP_VENDOR_SOURCES
|
||||
libs/fmt/src/format.cc
|
||||
libs/fmt/src/posix.cc
|
||||
libs/glad/src/glad.c
|
||||
libs/glad/src/glad_wgl.c
|
||||
libs/hash-library/md5.cpp
|
||||
libs/jpeg/jpgd.cpp
|
||||
libs/jpeg/jpge.cpp
|
||||
libs/nanort/nanort.cc
|
||||
libs/poly2tri/poly2tri/common/shapes.cc
|
||||
libs/poly2tri/poly2tri/sweep/advancing_front.cc
|
||||
libs/poly2tri/poly2tri/sweep/cdt.cc
|
||||
libs/poly2tri/poly2tri/sweep/sweep.cc
|
||||
libs/poly2tri/poly2tri/sweep/sweep_context.cc
|
||||
libs/sqlite3/sqlite3.c
|
||||
libs/tinyxml2/tinyxml2.cpp
|
||||
libs/wacom/WinTab/Utils.cpp
|
||||
libs/yoga/yoga/event/event.cpp
|
||||
libs/yoga/yoga/internal/experiments.cpp
|
||||
libs/yoga/yoga/log.cpp
|
||||
libs/yoga/yoga/Utils.cpp
|
||||
libs/yoga/yoga/YGConfig.cpp
|
||||
libs/yoga/yoga/YGEnums.cpp
|
||||
libs/yoga/yoga/YGLayout.cpp
|
||||
libs/yoga/yoga/YGNode.cpp
|
||||
libs/yoga/yoga/YGNodePrint.cpp
|
||||
libs/yoga/yoga/YGStyle.cpp
|
||||
libs/yoga/yoga/YGValue.cpp
|
||||
libs/yoga/yoga/Yoga.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/base64"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glad/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glm"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/hash-library"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/jpeg"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/nanort"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/headers"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/poly2tri/poly2tri"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/sqlite3"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
|
||||
)
|
||||
17
cmake/PanoPainterVersion.cmake
Normal file
17
cmake/PanoPainterVersion.cmake
Normal file
@@ -0,0 +1,17 @@
|
||||
function(pp_add_version_generation target config_name)
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py" "${config_name}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py"
|
||||
COMMENT "Generating src/version.gen.h"
|
||||
VERBATIM)
|
||||
|
||||
add_custom_target(pp_generate_version
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h")
|
||||
|
||||
add_dependencies(${target} pp_generate_version)
|
||||
endfunction()
|
||||
|
||||
44
cmake/PanoPainterWarnings.cmake
Normal file
44
cmake/PanoPainterWarnings.cmake
Normal file
@@ -0,0 +1,44 @@
|
||||
function(pp_configure_project_warnings target)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} INTERFACE
|
||||
/W4
|
||||
/permissive-
|
||||
/Zc:__cplusplus
|
||||
/Zc:preprocessor
|
||||
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||
/wd4100)
|
||||
if(PP_ENABLE_MSVC_ANALYZE)
|
||||
target_compile_options(${target} INTERFACE /analyze)
|
||||
endif()
|
||||
else()
|
||||
target_compile_options(${target} INTERFACE
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
-Wconversion
|
||||
-Wshadow
|
||||
-Wnull-dereference
|
||||
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||
-Wno-unused-parameter)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_ASAN)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} INTERFACE /fsanitize=address)
|
||||
target_link_options(${target} INTERFACE /fsanitize=address)
|
||||
else()
|
||||
target_compile_options(${target} INTERFACE -fsanitize=address)
|
||||
target_link_options(${target} INTERFACE -fsanitize=address)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_UBSAN AND NOT MSVC)
|
||||
target_compile_options(${target} INTERFACE -fsanitize=undefined)
|
||||
target_link_options(${target} INTERFACE -fsanitize=undefined)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_TSAN AND NOT MSVC)
|
||||
target_compile_options(${target} INTERFACE -fsanitize=thread)
|
||||
target_link_options(${target} INTERFACE -fsanitize=thread)
|
||||
endif()
|
||||
endfunction()
|
||||
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
75
cmake/ValidatePanoPainterShaders.cmake
Normal file
@@ -0,0 +1,75 @@
|
||||
if(NOT DEFINED PP_SHADER_DIR)
|
||||
message(FATAL_ERROR "PP_SHADER_DIR is required")
|
||||
endif()
|
||||
|
||||
file(REAL_PATH "${PP_SHADER_DIR}" pp_shader_dir)
|
||||
if(NOT IS_DIRECTORY "${pp_shader_dir}")
|
||||
message(FATAL_ERROR "Shader directory does not exist: ${pp_shader_dir}")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE pp_shader_files
|
||||
"${pp_shader_dir}/*.glsl")
|
||||
|
||||
if(NOT pp_shader_files)
|
||||
message(FATAL_ERROR "No shader files found under: ${pp_shader_dir}")
|
||||
endif()
|
||||
|
||||
set(pp_shader_errors "")
|
||||
set(pp_top_level_count 0)
|
||||
set(pp_include_count 0)
|
||||
|
||||
foreach(pp_shader_file IN LISTS pp_shader_files)
|
||||
file(RELATIVE_PATH pp_shader_rel "${pp_shader_dir}" "${pp_shader_file}")
|
||||
file(READ "${pp_shader_file}" pp_shader_contents)
|
||||
|
||||
string(REGEX MATCHALL "#[ \t]*include[ \t]+\"[^\"]+\"" pp_include_lines "${pp_shader_contents}")
|
||||
foreach(pp_include_line IN LISTS pp_include_lines)
|
||||
string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" pp_include_path "${pp_include_line}")
|
||||
if(pp_include_path MATCHES "^/")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must be relative: ${pp_include_path}")
|
||||
endif()
|
||||
if(pp_include_path MATCHES "^[A-Za-z]:")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not be drive-absolute: ${pp_include_path}")
|
||||
endif()
|
||||
if(pp_include_path MATCHES "\\.\\.")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not traverse parent directories: ${pp_include_path}")
|
||||
endif()
|
||||
if(NOT EXISTS "${pp_shader_dir}/${pp_include_path}")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: missing include: ${pp_include_path}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(pp_shader_rel MATCHES "^include/")
|
||||
math(EXPR pp_include_count "${pp_include_count} + 1")
|
||||
if(pp_shader_contents MATCHES "\\[\\[(vertex|fragment)\\]\\]")
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: include shaders must not declare stage markers")
|
||||
endif()
|
||||
else()
|
||||
math(EXPR pp_top_level_count "${pp_top_level_count} + 1")
|
||||
|
||||
string(REGEX MATCHALL "\\[\\[vertex\\]\\]" pp_vertex_markers "${pp_shader_contents}")
|
||||
string(REGEX MATCHALL "\\[\\[fragment\\]\\]" pp_fragment_markers "${pp_shader_contents}")
|
||||
list(LENGTH pp_vertex_markers pp_vertex_count)
|
||||
list(LENGTH pp_fragment_markers pp_fragment_count)
|
||||
|
||||
if(NOT pp_vertex_count EQUAL 1)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[vertex]] marker")
|
||||
endif()
|
||||
if(NOT pp_fragment_count EQUAL 1)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[fragment]] marker")
|
||||
endif()
|
||||
|
||||
string(FIND "${pp_shader_contents}" "[[vertex]]" pp_vertex_pos)
|
||||
string(FIND "${pp_shader_contents}" "[[fragment]]" pp_fragment_pos)
|
||||
if(pp_vertex_pos GREATER_EQUAL 0 AND pp_fragment_pos GREATER_EQUAL 0 AND NOT pp_vertex_pos LESS pp_fragment_pos)
|
||||
list(APPEND pp_shader_errors "${pp_shader_rel}: [[vertex]] marker must appear before [[fragment]]")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(pp_shader_errors)
|
||||
list(JOIN pp_shader_errors "\n" pp_shader_error_text)
|
||||
message(FATAL_ERROR "Shader validation failed:\n${pp_shader_error_text}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Validated ${pp_top_level_count} shader programs and ${pp_include_count} shader includes under ${pp_shader_dir}")
|
||||
49
docs/adr/0001-modernization-boundaries.md
Normal file
49
docs/adr/0001-modernization-boundaries.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ADR 0001: Incremental Component Boundaries
|
||||
|
||||
Status: accepted
|
||||
Date: 2026-05-31
|
||||
|
||||
## Context
|
||||
|
||||
PanoPainter currently has a flat `src/` layout with broad dependencies through
|
||||
`pch.h`, global singletons such as `App::I` and `Canvas::I`, OpenGL types in
|
||||
high-level painting/document headers, and duplicated platform source lists.
|
||||
The modernization work must retain existing behavior across Windows desktop
|
||||
and AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, and WebGL.
|
||||
|
||||
## Decision
|
||||
|
||||
Modernization will proceed incrementally. OpenGL remains the production
|
||||
renderer while component boundaries and tests are introduced. Vulkan, Metal,
|
||||
and WebGPU-related work must stay out of the production path until OpenGL
|
||||
parity tests exist.
|
||||
|
||||
The target dependency direction is:
|
||||
|
||||
```text
|
||||
pp_foundation
|
||||
-> pp_assets
|
||||
-> pp_paint
|
||||
-> pp_document
|
||||
-> pp_renderer_api
|
||||
-> pp_renderer_gl
|
||||
-> pp_paint_renderer
|
||||
-> pp_ui_core
|
||||
-> pp_panopainter_ui
|
||||
-> pp_platform_*
|
||||
-> panopainter_app
|
||||
```
|
||||
|
||||
Pure component headers must not include platform SDK headers or graphics API
|
||||
headers. Temporary shims are allowed only when recorded in
|
||||
`docs/modernization/debt.md`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The first implementation steps are documentation, inventory, CMake skeleton,
|
||||
diagnostics, and tests, not a renderer rewrite.
|
||||
- Existing project files remain until the shared CMake targets are validated.
|
||||
- Refactors should prefer additive compatibility layers before moving behavior.
|
||||
- Every extracted component must gain its own tests before the next component
|
||||
boundary is extracted.
|
||||
|
||||
1338
docs/modernization/build-inventory.md
Normal file
1338
docs/modernization/build-inventory.md
Normal file
File diff suppressed because it is too large
Load Diff
85
docs/modernization/capability-map.md
Normal file
85
docs/modernization/capability-map.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-05
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
is extracted, update the relevant rows with the owning component, test label,
|
||||
and validation command.
|
||||
|
||||
## Project And Documents
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture, live-canvas-to-`pp_document` snapshot projection with captured RGBA8 payloads, pending renderer-readback counts, save-readiness reporting before retained live saves, pure PPI export from payload-complete snapshots, and shared paint-renderer export-readiness reporting from the same snapshot |
|
||||
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
|
||||
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
|
||||
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
|
||||
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input, destination-feedback copy/fetch gate |
|
||||
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
|
||||
| App status and renderer diagnostics | App title/status widgets, extension indicators | `pp_app_core`, `pp_renderer_api`, `pp_panopainter_ui` | Title/status text tests, renderer diagnostic indicator tests, CLI status smoke, live layout adapter smoke |
|
||||
|
||||
## Image And Export
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
|
||||
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness through the shared paint-renderer export report, pure cube-face PNG writer, pure equirectangular PNG/JPEG+XMP writers, pure layer/frame collection PNG writers, app-core collection write executor, retained fallback coverage |
|
||||
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload/face-PNG readiness report, pure document-frame equirectangular PNG and JPEG+XMP export with live writer fallback, pure layer/frame equirectangular PNG collection export, exact GPU/golden parity |
|
||||
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, shared export-readiness report, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
|
||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer`, `pp_app_core` | Depth target/write planning, document-snapshot renderer-readiness logging, depth render-plan draw/readback counts, retained render/readback parity, and format/golden validation |
|
||||
|
||||
## Brush And Painting
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests |
|
||||
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
|
||||
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
|
||||
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, pure `pp_document` face and six-face frame compositing plus renderer texture upload/OpenGL command-plan coverage, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke/thumbnail/brush-preview destination-copy coverage, and GPU parity |
|
||||
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||
|
||||
## Layers And Animation
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Layer rename/add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document`, `pp_app_core` | Rename and operation planning, service-dispatch, no-op preservation, undo/redo invariant tests |
|
||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_app_core`, `pp_paint_renderer` | Metadata planning, service-dispatch, live-canvas-to-`pp_document` snapshot projection, CPU model and render golden |
|
||||
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
||||
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
||||
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
|
||||
|
||||
## UI And Workflow
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML |
|
||||
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
|
||||
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained overlay-adapter build coverage, retained popup/dialog lifetime tests |
|
||||
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
|
||||
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
|
||||
|
||||
## Input, Platform, And Devices
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback |
|
||||
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
|
||||
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
|
||||
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |
|
||||
| Desktop XR | `HMD`, `Vive`, `app_vr`, retained OpenVR bridge | `pp_platform_vr`, app with OpenXR backend | Runtime-selection policy tests, compile gate, and mocked pose tests |
|
||||
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
|
||||
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
|
||||
|
||||
## Cloud, Logging, And Automation
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Upload/download/browse | `app_cloud`, CURL helpers | `pp_app_core`, app service, `pp_platform_*` | Upload prompt/new-doc/no-canvas decision tests, bulk-upload progress decision tests, browse/selection decision tests, mocked HTTP and timeout tests |
|
||||
| License/check flows | app/cloud code | app service | Mocked response tests |
|
||||
| Logging/crash reporting | `log`, BugTrap/AppCenter | `pp_foundation`, platform wrappers | Log formatting and platform compile |
|
||||
| Headless automation | none yet | `tools/pano_cli` | JSON command fixtures |
|
||||
| Tracing | none yet | `pp_foundation` | Span nesting/timing tests |
|
||||
1391
docs/modernization/debt.md
Normal file
1391
docs/modernization/debt.md
Normal file
File diff suppressed because one or more lines are too long
233
docs/modernization/director-workflow.md
Normal file
233
docs/modernization/director-workflow.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Modernization Director Workflow
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-12
|
||||
|
||||
Use this workflow when the user explicitly asks for subagents, delegation, a
|
||||
director, captains, or parallel agent work. Do not spawn subagents just because
|
||||
a task is complex. The default is still one agent executing one task from
|
||||
`docs/modernization/tasks.md`.
|
||||
|
||||
## Goals
|
||||
|
||||
- Save main-thread tokens by keeping long exploration and simple edits out of
|
||||
the director context.
|
||||
- Use stronger models only where they change the outcome.
|
||||
- Keep each implementation slice measurable, validated, committed, and pushed.
|
||||
- Avoid merge conflicts by giving every agent a disjoint task and file scope.
|
||||
- Keep teams rolling: when one team finishes, integrate or park its result and
|
||||
start the next disjoint team without waiting for every other team to finish.
|
||||
- Prefer parallel execution whenever scopes are disjoint: run multiple teams at
|
||||
once, and give each team 2 or 3 workers when the work can be split cleanly.
|
||||
- Keep communication terse: no fillers, no cheerleading, no narrative padding.
|
||||
Use direct technical wording only.
|
||||
|
||||
## Roles
|
||||
|
||||
### Director
|
||||
|
||||
The director is the main agent in the user thread. The director owns:
|
||||
|
||||
- choosing the task group from `docs/modernization/tasks.md`
|
||||
- deciding whether delegation is worth the coordination cost
|
||||
- spawning one `gpt-5.4` captain per independent task group, including
|
||||
multiple teams when scopes are disjoint
|
||||
- integrating returned changes
|
||||
- running final validation
|
||||
- updating docs/debt/tasks
|
||||
- committing and pushing the verified slice
|
||||
- resetting conversation context after the slice when practical
|
||||
|
||||
The director should keep local work minimal. It should not implement production
|
||||
changes unless a required integration fix is smaller than another delegation
|
||||
round. Prefer giving captains ownership of implementation, focused validation,
|
||||
and worker coordination, then use the director context only for scope selection,
|
||||
conflict review, final validation, docs/task updates, commits, and pushes.
|
||||
|
||||
The director may do a quick local blocking check before delegation, such as
|
||||
reading task rows, checking git status, or confirming that two scopes do not
|
||||
overlap. If the next action is substantive code or test work, delegate it to a
|
||||
captain or worker whenever the scope can be made clear.
|
||||
|
||||
### Team Captain
|
||||
|
||||
Use `gpt-5.4` for each captain. A captain owns one coherent task group and is
|
||||
responsible for domain-level planning, for example:
|
||||
|
||||
- renderer/export boundary
|
||||
- legacy adapter retirement
|
||||
- platform/package parity
|
||||
- UI lifetime
|
||||
- dependency cleanup
|
||||
- test/hardening work
|
||||
|
||||
The captain owns implementation for its assigned task group. A team should be a
|
||||
captain plus multiple workers when the work can be split into disjoint files.
|
||||
The captain turns one task-group objective, or a small coherent run of adjacent
|
||||
task rows, into 2 or 3 smaller disjoint subtasks when possible, spawns or
|
||||
requests workers when useful, reviews their changes, runs focused validation
|
||||
when cheap, and returns an integration-ready result. If nested subagents are
|
||||
available, the captain may spawn workers and continue through the next
|
||||
compatible subtask after each worker returns. If nested subagents are not
|
||||
available in the current surface, the captain returns a worker plan and the
|
||||
director performs the second-level spawns without taking over implementation.
|
||||
|
||||
The captain must not edit broad shared files unless assigned. The captain must
|
||||
not rewrite the task tracker or roadmap except for a requested status note.
|
||||
|
||||
### Workers And Explorers
|
||||
|
||||
Workers perform bounded edits in assigned files. Explorers answer specific
|
||||
questions and should usually not edit files.
|
||||
|
||||
Every worker and explorer must be told:
|
||||
|
||||
- this repository may have other agents working in parallel
|
||||
- do not revert or overwrite unrelated changes
|
||||
- stay inside the assigned scope
|
||||
- focus on the assigned task first; do not start by reading unrelated docs or
|
||||
broad repository areas unless the task explicitly requires it
|
||||
- report changed files, validation run, and blockers
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Work Type | Model | Reasoning Effort | Use |
|
||||
| --- | --- | --- | --- |
|
||||
| Director orchestration and integration | `gpt-5.4-mini` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes. |
|
||||
| Team captain for a domain slice | `gpt-5.4` | `medium` default, `high` for renderer/platform architecture | Split a broad domain slice into worker-sized tasks, supervise workers, review results, and integrate changes. |
|
||||
| Risky implementation with cross-file C++ behavior | `gpt-5.4` | `medium` or `high` | Code changes touching contracts, build graph, or live adapters. |
|
||||
| Bounded implementation in known files | `gpt-5.4-mini` | `medium` | Localized tests, simple adapters, docs/debt updates, small refactors. |
|
||||
| Fast lookup or inventory | `gpt-5.3-codex-spark` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
|
||||
| Mechanical docs cleanup | `gpt-5.3-codex-spark` | `low` | Formatting, table updates, command list normalization. |
|
||||
| Ambiguous strategy or failed captain escalation | `gpt-5.5` | inherited or `high` | Only when cheaper models are likely to make architectural mistakes. |
|
||||
|
||||
Prefer the inherited model unless the user requested this director workflow or
|
||||
there is a clear task-specific reason to override. In this workflow, the user
|
||||
has requested `gpt-5.4` team leads, so use that override for captains and keep
|
||||
the director on `gpt-5.4-mini` for orchestration unless a higher-reasoning
|
||||
escalation is justified.
|
||||
|
||||
## Token Discipline
|
||||
|
||||
- Use `fork_context=false` by default. Pass the task id, relevant files, debt
|
||||
ids, validation commands, and only the necessary excerpts.
|
||||
- Use `fork_context=true` only when prior conversation details are essential and
|
||||
not already in repo docs.
|
||||
- Do not paste large logs into prompts. Point agents at log paths and ask for
|
||||
the smallest relevant excerpt.
|
||||
- Do not ask captains or workers to "read the roadmap" generally. Name the
|
||||
exact task row and debt ids.
|
||||
- Keep subagent prompts under about 250 words for explorers and 500 words for
|
||||
workers/captains unless the task truly needs more.
|
||||
- Ask for compact final reports: changed files, result, validation, blockers,
|
||||
next recommendation.
|
||||
- Keep final reports minimal and technical.
|
||||
- Close completed agents after their results are integrated or rejected.
|
||||
- Prefer the smallest number of teams that keeps disjoint work moving. Multiple
|
||||
captains are appropriate when task rows have non-overlapping write scopes and
|
||||
can validate independently. Avoid many agents in one file family, but do use
|
||||
multiple teams when the scopes are disjoint and the work can advance in
|
||||
parallel.
|
||||
- Do not synchronize all teams at a barrier unless validation or merge risk
|
||||
requires it. Use rolling integration: wait for whichever team finishes first,
|
||||
process that result, then immediately start another disjoint team if ready
|
||||
work remains.
|
||||
|
||||
## Delegation Flow
|
||||
|
||||
1. Director picks two or more `Ready` tasks from
|
||||
`docs/modernization/tasks.md` with disjoint write scopes.
|
||||
2. Director assigns each independent task group to a `gpt-5.4` captain, with
|
||||
preference for 2 or 3 worker subtasks per team when the scope can be split
|
||||
cleanly.
|
||||
3. Captains implement directly or coordinate workers/explorers for disjoint
|
||||
subtasks, and may continue through a coherent sequence of adjacent tasks
|
||||
within the assigned scope.
|
||||
4. Captains run focused validation when cheap and return changed files,
|
||||
validation, blockers, and integration notes.
|
||||
5. Director waits for whichever team finishes first, reviews for scope
|
||||
conflicts, integrates returned changes, and starts the next disjoint team
|
||||
while other teams keep running.
|
||||
6. Director runs the listed validation command or the quiet checkpoint wrapper
|
||||
for each integrated slice.
|
||||
7. Director updates `tasks.md`, `debt.md`, and `roadmap.md` if tasks moved.
|
||||
8. Director commits and pushes verified slices incrementally.
|
||||
|
||||
## Director Prompt Template For A Captain
|
||||
|
||||
```text
|
||||
You are the gpt-5.4 team captain for the <group> task group in PanoPainter.
|
||||
|
||||
Task source: docs/modernization/tasks.md task(s) <TASK-ID-LIST>.
|
||||
Debt ids: <DEBT-LIST>.
|
||||
Assigned scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
|
||||
Goal: <ONE PARAGRAPH>.
|
||||
|
||||
Own this task group through planning and implementation. Build a small team of
|
||||
`gpt-5.4-mini` workers for disjoint subtasks when possible, review their
|
||||
outputs, and keep looping through the assigned coherent task sequence until the
|
||||
scope is done, blocked, or no longer safe to continue. Use `gpt-5.4` for broad
|
||||
domain planning, interface decisions, and risky C++ behavior changes. Keep
|
||||
tasks small enough to validate. Do not edit outside the assigned scope. Other
|
||||
agents may be working in parallel; do not revert unrelated changes.
|
||||
|
||||
Return:
|
||||
- recommended worker tasks with model choice and file scope
|
||||
- completed edits or the exact blocker preventing them
|
||||
- focused validation run and result
|
||||
- risks/blockers
|
||||
- integration notes for the director
|
||||
```
|
||||
|
||||
## Captain Prompt Template For A Worker
|
||||
|
||||
```text
|
||||
You are a worker on PanoPainter. Other agents may be editing nearby files; do
|
||||
not revert unrelated changes.
|
||||
|
||||
Task: <WORKER-TASK>.
|
||||
Model fit: <WHY THIS MODEL IS ENOUGH>.
|
||||
Write scope: <FILES/DIRS ONLY>.
|
||||
Read scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
|
||||
Make the smallest behavior-preserving change that satisfies the done checks.
|
||||
Do not spend tokens on broad document review or inventory outside the assigned
|
||||
scope unless the task requires it.
|
||||
Update tests/docs only if listed. If the task is larger than expected, stop and
|
||||
report the split instead of broadening scope.
|
||||
|
||||
Final report:
|
||||
- files changed
|
||||
- behavior changed
|
||||
- validation run and result
|
||||
- blockers or follow-up
|
||||
```
|
||||
|
||||
## Explorer Prompt Template
|
||||
|
||||
```text
|
||||
Answer one codebase question for PanoPainter.
|
||||
|
||||
Question: <QUESTION>.
|
||||
Search scope: <FILES/DIRS>.
|
||||
Use compiler-aware navigation if this depends on C++ symbols.
|
||||
Do not edit files.
|
||||
|
||||
Return only:
|
||||
- answer
|
||||
- supporting file references
|
||||
- confidence and caveats
|
||||
```
|
||||
|
||||
## Final Integration Checklist
|
||||
|
||||
- No worker changed files outside its assigned scope without calling it out.
|
||||
- No generated logs or build output were committed.
|
||||
- Focused validation for the task passed or the failure is documented.
|
||||
- `docs/modernization/debt.md` changed when debt was narrowed or closed.
|
||||
- `docs/modernization/tasks.md` score changed only for `Done` tasks.
|
||||
- The commit contains one coherent slice.
|
||||
- The branch was pushed.
|
||||
3333
docs/modernization/roadmap.md
Normal file
3333
docs/modernization/roadmap.md
Normal file
File diff suppressed because it is too large
Load Diff
787
docs/modernization/tasks.md
Normal file
787
docs/modernization/tasks.md
Normal file
@@ -0,0 +1,787 @@
|
||||
# Modernization Task Tracker
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-13
|
||||
|
||||
This file turns the modernization roadmap into small, measurable work items.
|
||||
The roadmap explains direction, the debt log explains why shortcuts remain, and
|
||||
this tracker is the execution queue. Prefer closing one task here over adding a
|
||||
new broad roadmap paragraph.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Pick one `Ready` task at a time unless the user asks for planning only.
|
||||
- Keep each slice small enough to validate and commit in one session.
|
||||
- Do not claim percentage progress for "narrowed" debt. Points move only when a
|
||||
task row is changed to `Done`.
|
||||
- A task is `Done` only when its listed checks pass, the debt log is updated or
|
||||
closed as applicable, and the roadmap/task score is updated.
|
||||
- If a task proves too large, split it before editing code. The original task
|
||||
stays `Ready` or becomes `Blocked` with the reason.
|
||||
- After a verified task is committed and pushed, reset conversation context
|
||||
before starting the next task when practical.
|
||||
- When the user asks for subagents or delegation, follow
|
||||
`docs/modernization/director-workflow.md` and keep each delegated task mapped
|
||||
to a row in this tracker.
|
||||
|
||||
## Progress Scorecard
|
||||
|
||||
The current score is intentionally conservative. It should move in visible,
|
||||
auditable steps rather than by subjective estimates.
|
||||
|
||||
| Area | Weight | Current | Progress Rule |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Build and CMake ownership | 15 | 13 | Root CMake owns active source lists, app/tool targets, and retained package entrypoints. |
|
||||
| Test and automation coverage | 15 | 9 | Headless, platform, package, and focused validation commands exist and are current. |
|
||||
| Pure component behavior ownership | 15 | 8 | Behavior lives in `pp_*` components and is consumed by live adapters. |
|
||||
| Legacy adapter retirement | 20 | 7 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. |
|
||||
| Renderer boundary and OpenGL parity | 15 | 10 | Live render/export/readback paths execute through renderer interfaces with parity checks. |
|
||||
| Platform and package parity | 10 | 6 | Required platforms have root CMake/package validation and injected platform services. |
|
||||
| Hardening and future backend readiness | 10 | 2 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. |
|
||||
| **Total** | **100** | **55** | Only completed tasks below may change this number. |
|
||||
|
||||
When updating `Current`, add a dated note under "Completed Task Log" with the
|
||||
task id, points moved, validation command, and commit hash.
|
||||
|
||||
## Task States
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| `Ready` | Clear enough for an agent to execute. |
|
||||
| `In progress` | Actively being changed in the current slice. |
|
||||
| `Blocked` | Needs a user decision, missing toolchain, or a prior task. |
|
||||
| `Done` | Validated, documented, committed, and pushed. |
|
||||
|
||||
## Ready Queue
|
||||
|
||||
### MT-001 - Adopt Measurable Task Tracking
|
||||
|
||||
Status: Done
|
||||
Score: no score movement
|
||||
Debt: none
|
||||
Scope: `docs/modernization/tasks.md`, `docs/modernization/roadmap.md`
|
||||
|
||||
Steps:
|
||||
|
||||
- Add this tracker.
|
||||
- Link it from the roadmap.
|
||||
- Make the scorecard the source for percentage claims.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `docs/modernization/roadmap.md` points agents to this file.
|
||||
- The tracker has task states, scoring rules, and at least one ready queue.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
git diff -- docs\modernization\roadmap.md docs\modernization\tasks.md
|
||||
```
|
||||
|
||||
### ADP-001 - Remove History Bridge From Document Resize And Canvas Clear
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0020`, `DEBT-0027`
|
||||
Scope: `src/legacy_document_canvas_services.*`,
|
||||
`src/app_core/document_resize.h`, `tests/app_core/document_resize_tests.cpp`,
|
||||
related canvas-clear tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Make document resize and canvas-clear execution consume app-core history
|
||||
commands directly instead of routing through `legacy_history_services`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_canvas_services.*` no longer includes
|
||||
`legacy_history_services.h`.
|
||||
- Resize still executes in order: resize, title update, history clear.
|
||||
- Canvas clear still records undo and marks the document unsaved when a canvas
|
||||
exists.
|
||||
- `docs/modernization/debt.md` narrows or closes the affected removal condition.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_resize|pp_app_core_document_canvas|pano_cli_plan_document_resize|pano_cli_plan_canvas_clear" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-002 - Remove History Bridge From Layer Operations
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0021`
|
||||
Scope: `src/legacy_document_layer_services.*`,
|
||||
`src/app_core/document_layer.h`, `tests/app_core/document_layer_tests.cpp`
|
||||
|
||||
Goal:
|
||||
|
||||
Move layer add/remove/merge/clear/rename history side effects into tested
|
||||
app-core execution plans so the live layer bridge no longer calls
|
||||
`legacy_history_services`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_layer_services.*` no longer includes
|
||||
`legacy_history_services.h`.
|
||||
- Layer operations still preserve undo/history behavior covered by
|
||||
`pp_app_core_document_layer_tests`.
|
||||
- `pano_cli plan-layer-operation`, `plan-layer-menu`, and
|
||||
`plan-layer-rename` JSON remains compatible.
|
||||
- `docs/modernization/debt.md` records the narrowed or closed layer-history
|
||||
adapter dependency.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer|pano_cli_plan_layer" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-003 - Remove History Bridge From Document Open And Session Save
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0039`, `DEBT-0040`, `DEBT-0042`
|
||||
Scope: `src/legacy_document_open_services.*`,
|
||||
`src/legacy_document_session_services.*`,
|
||||
`src/app_core/document_session.*`, `src/app_core/document_route.*`,
|
||||
matching tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Make document-open, close, save, save-before-workflow, Save As, and Save Version
|
||||
history effects explicit app-core outputs instead of direct
|
||||
`legacy_history_services` calls in the live bridges.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_open_services.*` and
|
||||
`src/legacy_document_session_services.*` no longer include
|
||||
`legacy_history_services.h`.
|
||||
- Existing dirty-document, save-before, new-document, Save As, and Save Version
|
||||
plans preserve their JSON contracts.
|
||||
- The debt log is updated for every debt id listed above.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_route|pp_app_core_document_session|pano_cli_plan_open_route|pano_cli_simulate_app_session|pano_cli_plan_document_file|pano_cli_plan_document_version" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-004 - Make Dialog Creation A UI Factory Boundary
|
||||
|
||||
Status: Done
|
||||
Score: +2 legacy adapter retirement
|
||||
Debt: `DEBT-0058`, `DEBT-0063`
|
||||
Scope: `src/legacy_app_dialog_services.*`,
|
||||
`src/legacy_ui_overlay_services.*`, `src/app_dialogs.cpp`,
|
||||
`src/app_core/app_dialog.h`, dialog tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Keep app-core dialog metadata pure, but move retained
|
||||
`NodeProgressBar`/`NodeMessageBox`/`NodeInputBox` construction behind one
|
||||
`pp_panopainter_ui` or retained UI factory function. `App` should ask for a
|
||||
dialog object through an interface instead of knowing individual node creation
|
||||
details.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `App::show_progress`, `App::message_box`, and `App::input_box` still preserve
|
||||
captions, cancel behavior, and keyboard behavior.
|
||||
- New factory path has focused tests or existing `pp_app_core_app_dialog_tests`
|
||||
plus a smoke command proving the live adapter still builds.
|
||||
- The debt log states exactly which raw-node lifetime hazards remain.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_app_dialog|pano_cli_plan_app_dialog" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-005 - Convert One Popup/Dialog Family To Checked Overlay Lifetime
|
||||
|
||||
Status: Done
|
||||
Score: +2 legacy adapter retirement
|
||||
Debt: `DEBT-0063`
|
||||
Scope: choose exactly one family from `src/node_dialog_open.cpp`,
|
||||
`src/node_dialog_browse.cpp`, `src/node_panel_quick.cpp`,
|
||||
`src/node_panel_stroke.cpp`, or `src/node_combobox.cpp`, plus
|
||||
`src/legacy_ui_overlay_services.*`
|
||||
|
||||
Goal:
|
||||
|
||||
Adopt `pp_ui_core` overlay lifetime semantics for one retained popup/dialog
|
||||
family before trying to rewrite all retained UI nodes.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- The chosen family no longer owns open-coded root insertion, outside-click
|
||||
release, close callback wiring, or destroy-during-callback behavior.
|
||||
- Existing UI behavior is preserved.
|
||||
- Tests cover missing root/template handling and close/release behavior for the
|
||||
chosen family.
|
||||
- The debt log names the completed family and the remaining families.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_ui_core_overlay_lifetime|pp_ui_core_node_lifetime" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||
```
|
||||
|
||||
### RND-001 - Make Pure Equirectangular Export The Primary Success Path
|
||||
|
||||
Status: Done
|
||||
Score: +2 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/legacy_document_export_services.*`,
|
||||
`src/app_core/document_export.*`, `src/paint_renderer/compositor.*`,
|
||||
`tests/app_core/document_export_tests.cpp`,
|
||||
`tests/paint_renderer/compositor_tests.cpp`
|
||||
|
||||
Goal:
|
||||
|
||||
For payload-complete snapshots, live PNG/JPEG equirectangular export should
|
||||
complete through the pure document/paint-renderer writer. Retained
|
||||
`Canvas::export_equirectangular*` should run only for unsupported Web,
|
||||
incomplete-readback, or writer-failure fallbacks.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Export route reports whether the pure writer was used or why fallback was
|
||||
required.
|
||||
- Tests cover pure-writer success and each fallback reason.
|
||||
- Live bridge does not call retained equirectangular export after pure-writer
|
||||
success.
|
||||
- The debt log narrows retained equirectangular export execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route|pano_cli_simulate_document_export" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-002 - Make Pure Layer And Animation Collection Export Primary
|
||||
|
||||
Status: Done
|
||||
Score: +2 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/legacy_document_export_services.*`,
|
||||
`src/app_core/document_export.*`, `src/paint_renderer/compositor.*`,
|
||||
collection export tests only
|
||||
|
||||
Goal:
|
||||
|
||||
For payload-complete snapshots, live layer and animation-frame collection export
|
||||
should complete through the pure collection writer. Retained
|
||||
`Canvas::export_layers*` and `Canvas::export_anim_frames*` should run only for
|
||||
unsupported Web, incomplete-readback, or writer-failure fallbacks.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Collection export route reports pure-writer success versus fallback reason.
|
||||
- Tests cover layer collection, animation-frame collection, and fallback.
|
||||
- The debt log narrows retained collection export execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-003 - Replace Depth Export Readiness With Pure Depth Export Execution
|
||||
|
||||
Status: Done
|
||||
Score: +3 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/paint_renderer/compositor.*`,
|
||||
`src/app_core/document_export.*`, `src/legacy_document_export_services.*`,
|
||||
depth tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Turn the current pure depth export render plan into actual payload generation
|
||||
for payload-complete snapshots, then write image/depth payloads through the
|
||||
existing app-core two-payload writer before falling back to retained
|
||||
`Canvas::export_depth*`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Pure depth export produces deterministic image and depth payloads for a
|
||||
payload-complete snapshot.
|
||||
- Retained depth export runs only for unsupported targets, incomplete readback,
|
||||
or writer failure.
|
||||
- Tests cover malformed depth target inputs and byte-size validation.
|
||||
- The debt log narrows depth export readback/execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-004 - Add First Desktop GPU Golden Gate
|
||||
|
||||
Status: Done
|
||||
Score: +2 hardening and future backend readiness
|
||||
Debt: `DEBT-0036`
|
||||
Scope: `tests/`, `CMakeLists.txt`, renderer test helpers only
|
||||
|
||||
Goal:
|
||||
|
||||
Create the first non-default `desktop-gpu` golden/readback test that validates
|
||||
one OpenGL output against a deterministic fixture. Keep it opt-in so headless
|
||||
agents are not blocked.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `ctest --preset desktop-gpu --build-config Debug` has at least one real test.
|
||||
- The test is skipped with a clear message when no GPU/context is available.
|
||||
- The roadmap and debt log describe what the golden covers and what remains.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-gpu --build-config Debug --output-on-failure
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_renderer_gl|pp_paint_renderer" --output-on-failure
|
||||
```
|
||||
|
||||
### PLT-001 - Split Apple Picker/Browse Service From Legacy Platform Adapter
|
||||
|
||||
Status: Done
|
||||
Score: +2 platform and package parity
|
||||
Debt: `DEBT-0017`, `DEBT-0051`, `DEBT-0055`
|
||||
Scope: `src/platform_legacy/legacy_platform_services.*`, new
|
||||
`src/platform_apple/*` files if needed, `CMakeLists.txt`, platform API tests
|
||||
|
||||
Goal:
|
||||
|
||||
Move macOS/iOS document browse roots, file picking, directory picking, and
|
||||
display-path formatting out of the catch-all legacy platform adapter and into a
|
||||
named Apple platform service boundary.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/platform_legacy/legacy_platform_services.*` no longer owns Apple
|
||||
browse/picker policy.
|
||||
- Apple compile validation still passes through the remote build script.
|
||||
- The debt log narrows Apple platform shell extraction.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device
|
||||
```
|
||||
|
||||
### PLT-002 - Split Web Export/Storage Policy From Legacy Platform Adapter
|
||||
|
||||
Status: Done
|
||||
Score: +2 platform and package parity
|
||||
Debt: `DEBT-0050`, `DEBT-0053`, `DEBT-0057`
|
||||
Scope: `src/platform_legacy/legacy_platform_services.*`, new
|
||||
`src/platform_web/*` files if needed, `src/platform_api/*`, platform tests
|
||||
|
||||
Goal:
|
||||
|
||||
Move WebGL exported-image publishing, persistent-storage flushing,
|
||||
prepared-file handoff, and default canvas resolution out of the catch-all
|
||||
legacy platform adapter into a named Web platform service boundary.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Web policy is injectable through `pp_platform_api`.
|
||||
- The legacy adapter no longer owns Web default canvas resolution or storage
|
||||
flush policy.
|
||||
- Package-smoke readiness still reports Web blockers explicitly.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly
|
||||
```
|
||||
|
||||
### DEP-001 - Remove Generated fmt Overlay
|
||||
|
||||
Status: Done
|
||||
Score: +1 build and CMake ownership
|
||||
Debt: `DEBT-0062`
|
||||
Scope: `CMakeLists.txt`, `cmake/`, `vcpkg.json`, `libs/fmt` or package wiring
|
||||
|
||||
Goal:
|
||||
|
||||
Use a supported fmt package or update the vendored fmt release so VS 2026 no
|
||||
longer needs a generated `format.h` overlay.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- No build-tree fmt header overlay is generated.
|
||||
- `DEBT-0062` is closed.
|
||||
- Windows app and at least one focused component test build pass.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_platform_api_tests
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
```
|
||||
|
||||
### DEP-002 - Remove Generated nanort Overlay
|
||||
|
||||
Status: Done
|
||||
Score: +1 build and CMake ownership
|
||||
Debt: `DEBT-0060`
|
||||
Scope: retained Android package CMake, `libs/nanort`, grid/lightmap dependency
|
||||
wiring
|
||||
Goal:
|
||||
|
||||
Update, replace, or isolate `nanort` so Android package builds do not generate
|
||||
a patched vendor overlay.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Android retained package CMake no longer generates a patched `nanort.h`.
|
||||
- `DEBT-0060` is closed.
|
||||
- Standard Android retained package validation passes.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||
```
|
||||
|
||||
## Blocked Or Later Queue
|
||||
|
||||
These are real goals, but they should not be picked until prerequisite tasks
|
||||
above have reduced risk.
|
||||
|
||||
### LATER-001 - Replace OpenVR With OpenXR
|
||||
|
||||
Status: Blocked
|
||||
Score: +3 platform and package parity
|
||||
Debt: `DEBT-0061`
|
||||
Blocked By: OpenXR package decision and runtime availability
|
||||
|
||||
Done Checks:
|
||||
|
||||
- OpenXR SDK/package target exists.
|
||||
- Windows platform service can start/stop desktop XR without OpenVR.
|
||||
- `libs/openvr` and `openvr_api.dll` deployment are removed.
|
||||
- `DEBT-0061` is closed.
|
||||
|
||||
### LATER-002 - Remove Catch2 Harness Debt
|
||||
|
||||
Status: Blocked
|
||||
Score: +2 test and automation coverage
|
||||
Debt: `DEBT-0005`
|
||||
Blocked By: vcpkg/toolchain reliability across Windows and headless presets
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Existing local test harness is replaced or permanently justified.
|
||||
- Catch2 tests run through `desktop-fast` and vcpkg headless presets.
|
||||
- `DEBT-0005` is closed.
|
||||
|
||||
### LATER-003 - Live Stroke Rasterization Through Renderer Services
|
||||
|
||||
Status: Ready
|
||||
Score: +5 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0036`
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Live stroke rasterization, dual-brush compositing, and pattern feedback choose
|
||||
paths through renderer services.
|
||||
- OpenGL output parity is covered by golden/readback tests.
|
||||
- Retained stroke OpenGL execution is deleted or isolated as an OpenGL backend
|
||||
implementation.
|
||||
|
||||
Progress Notes:
|
||||
|
||||
- 2026-06-13: `Canvas::draw_merge()` now routes per-plane merge-target clear,
|
||||
blend-state gating, and optional checkerboard prepass through
|
||||
`execute_legacy_canvas_draw_merge_plane_setup(...)`; per-layer iteration,
|
||||
temporary-stroke branches, and concrete framebuffer ownership remain local to
|
||||
`Canvas`. Next slice should target another narrow draw-merge execution seam
|
||||
without reopening landed plane-setup, temporary-composite, layer-blend,
|
||||
final-plane, or texture-alpha helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` now routes the standard per-layer
|
||||
texture-alpha pass through
|
||||
`execute_legacy_canvas_draw_merge_layer_texture(...)`; temporary-stroke
|
||||
branches, per-plane iteration, and concrete RTT ownership remain local to
|
||||
`Canvas`. Next slice should target another narrow draw-merge execution seam
|
||||
without reopening landed temporary-composite, layer-blend, final-plane, or
|
||||
texture-alpha helpers.
|
||||
- 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers
|
||||
retained frame-plan assembly for previous-sample projection mode and zoom
|
||||
scaling. Next slice should target the remaining preview/Canvas stroke
|
||||
execution seam or another narrow renderer boundary without reopening the
|
||||
landed stroke-frame planner coverage.
|
||||
- 2026-06-13: `NodeStrokePreview` live-pass orchestration now routes through
|
||||
`execute_legacy_stroke_preview_live_pass(...)`, and
|
||||
`pp_paint_renderer_stroke_execution_tests` now covers live-pass clear,
|
||||
traversal, and final copy ordering. Next slice should target the remaining
|
||||
preview or Canvas stroke execution seam without reopening the landed live-pass
|
||||
helper.
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes retained
|
||||
preview feedback/material/composite planning plus stroke-shader uniform
|
||||
assembly through `plan_legacy_node_stroke_preview_pass_orchestration(...)`;
|
||||
compositor coverage now locks destination-feedback fallback, composite-slot
|
||||
intent, and the retained pattern/dual shader-uniform handoff. The preview
|
||||
node still owns brush object mutation, retained `Stroke` population, and the
|
||||
concrete GL pass callbacks. Next slice should target another narrow preview
|
||||
execution seam without reopening the landed preview setup, mix, pass-sequence,
|
||||
or final-composite helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes preview
|
||||
stroke max-size fallback, dual-preview max-size derivation, pattern-scale
|
||||
flips, and Bezier preview-point generation through
|
||||
`plan_legacy_node_stroke_preview_stroke_setup(...)`; compositor coverage now
|
||||
also locks the retained stroke-setup curve/pressure intent and
|
||||
pass-sequence request-validation short-circuit behavior. The preview node
|
||||
still owns brush object mutation, camera wiring, retained `Stroke`
|
||||
population, and live GL execution. Next slice should target the remaining
|
||||
higher-level preview pass orchestration seam without reopening landed
|
||||
sample, mix, pass-sequence, or final-composite helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` per-layer blend composite now routes
|
||||
merge-RTT unbind, shader setup, optional destination-copy feedback, draw,
|
||||
and cleanup ordering through
|
||||
`execute_legacy_canvas_draw_merge_layer_blend(...)`; temporary-stroke
|
||||
branch selection, layer iteration, and final merged-plane redraw ownership
|
||||
remain local to `Canvas`. Next slice should target the remaining end-of-plane
|
||||
merged-texture copy/grid/final-redraw seam or another similarly narrow final
|
||||
composite boundary without reopening landed temporary-composite or
|
||||
per-layer blend helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` end-of-plane merged-texture copy,
|
||||
optional checkerboard redraw, and final merged-texture composite ordering now
|
||||
route through `execute_legacy_canvas_draw_merge_final_plane_composite(...)`;
|
||||
per-plane iteration and concrete framebuffer, sampler, texture, and draw
|
||||
callbacks remain local to `Canvas`. Next slice should target another narrow
|
||||
retained draw-merge boundary without reopening landed temporary-composite,
|
||||
layer-blend, or final-plane helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` erase live temporary-stroke composite now
|
||||
routes retained setup, sampler bind, texture bind, draw, and texture unbind
|
||||
ordering through `execute_legacy_canvas_stroke_temporary_composite(...)`;
|
||||
both live temporary composite branches now share the same retained execution
|
||||
helper while `Canvas` keeps only concrete GL object callbacks and broader
|
||||
final composite ownership. Next slice should target the remaining final
|
||||
composite seam without reopening landed sample, mix, dirty, or framebuffer
|
||||
helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::stroke_draw_mix()` now routes retained
|
||||
mix-pass shader setup plus framebuffer/state/input/draw ordering through
|
||||
`execute_legacy_node_stroke_preview_mix_pass(...)`, with compositor coverage
|
||||
locking the retained callback order and shader-plan handoff. The preview node
|
||||
keeps only the concrete GL save/restore, texture-object bind, and plane-draw
|
||||
callbacks. Next slice should target another retained preview or canvas stroke
|
||||
seam without reopening the landed preview sample, material-planning,
|
||||
pass-sequence, or final-composite helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes
|
||||
dual-pass/background/main-pass/final-composite/copy-back ordering through
|
||||
`execute_legacy_node_stroke_preview_pass_sequence(...)`; the remaining local
|
||||
preview ownership is concentrated around `stroke_draw_mix()` setup/execution.
|
||||
Next slice should target that final mix-pass setup/execution seam without
|
||||
reopening landed sample, binding, material-planning, or final-composite
|
||||
helpers.
|
||||
- 2026-06-13: `Canvas::draw_merge()` non-erase live temporary-stroke
|
||||
composite ordering now routes through
|
||||
`execute_legacy_canvas_stroke_temporary_composite(...)`; erase-path and
|
||||
broader final composite ownership still remain local to `Canvas`. Next slice
|
||||
should target the erase temporary composite branch or another similarly
|
||||
narrow final composite seam without reopening landed sample, mix, dirty, or
|
||||
framebuffer helpers.
|
||||
- 2026-06-13: `Canvas::stroke_draw_samples()` now routes face-indexed
|
||||
destination bind/copy/unbind and brush upload/draw through
|
||||
`execute_legacy_canvas_stroke_face_sample_polygon(...)`; the retained sample
|
||||
executor now owns the face-aware dispatch contract while `Canvas` keeps only
|
||||
the concrete GL object callbacks. Next slice should target any remaining
|
||||
final temporary-texture composite setup without reopening landed sample,
|
||||
mix, sampler, dirty, face, or pad helpers.
|
||||
- 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers
|
||||
direct retained frame-sample callback ordering plus
|
||||
`execute_legacy_canvas_stroke_frame_samples_with_dirty_tracking(...)`
|
||||
timing/state semantics, including pre-update dirty visibility and
|
||||
`previous_pass_dirty_box` override behavior. Next test slice should target
|
||||
the next header-level preview live sample ordering surface without reopening
|
||||
production files.
|
||||
- 2026-06-13: `Canvas::stroke_draw_mix()` now routes visible-plane filtering,
|
||||
retained sampler/texture-slot binding, and final plane draw ordering through
|
||||
`execute_legacy_canvas_stroke_mix_pass(...)`; mixer framebuffer/state setup
|
||||
and per-plane shader material/MVP preparation remain local to `Canvas`. Next
|
||||
slice should target the remaining `stroke_draw_samples()` callback body or
|
||||
any final temporary-texture composite setup without reopening landed
|
||||
sample, sampler, dirty, face, or pad helpers.
|
||||
- 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers
|
||||
retained preview background capture ordering, final composite ordering, and
|
||||
preview texture-copy bind-before-copy behavior via
|
||||
`legacy_canvas_stroke_preview_services.h`. Next test slice should target the
|
||||
next header-level preview live sample or mix-pass ordering surface without
|
||||
reopening production files.
|
||||
- 2026-06-13: `Canvas::stroke_draw_samples()` now routes polygon
|
||||
triangulation, sample-point assembly, and the retained destination-copy /
|
||||
upload / draw helper handoff through
|
||||
`execute_legacy_canvas_stroke_sample_polygon(...)`; direct GL callback
|
||||
wiring and the remaining live draw ownership stay local to `Canvas`. Next
|
||||
slice should target the remaining callback body or final temporary-texture
|
||||
composite setup without reopening the landed sampler, dirty, face, or pad
|
||||
helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::stroke_draw_mix()` now routes mixer
|
||||
framebuffer bind/unbind, viewport/scissor/blend state, texture-slot binding,
|
||||
and final plane draw through one local helper; material planning and shader
|
||||
uniform setup remain local to the preview node. Next slice should target the
|
||||
remaining mix-pass material/setup orchestration without reopening the landed
|
||||
preview live-pass, binding, sample, or final composite helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::stroke_draw_mix()` now routes retained
|
||||
mix-pass material planning plus composite/pattern/dual uniform payload
|
||||
assembly through `plan_legacy_node_stroke_preview_mix_pass(...)`, with
|
||||
compositor coverage locking the retained preview mix adapter semantics for
|
||||
composite-pass patterning and dual state. Mixer framebuffer ownership,
|
||||
retained shader execution, texture binding, and final draw/copy ordering
|
||||
remain local to the preview node. Next slice should target the remaining
|
||||
mix-pass execution ownership without reopening the landed preview live-pass,
|
||||
binding, sample, or final composite helpers.
|
||||
- 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers
|
||||
retained texture-dispatch activation order and sampler-dispatch routing
|
||||
across brush tip, destination, pattern, and mixer helper inputs. Next test
|
||||
slice should extend the dedicated lane into frame-sample loop ordering or
|
||||
preview-side retained helper coverage.
|
||||
- 2026-06-13: Added `pp_paint_renderer_stroke_execution_tests` as a dedicated
|
||||
retained stroke execution-helper target covering texture-input binding order,
|
||||
sample destination-copy behavior, live-pass face-framebuffer dirty tracking,
|
||||
and pad-face destination-copy behavior. Next test slice should extend this
|
||||
target toward the newer sampler-dispatch helpers or preview-side retained
|
||||
execution helpers.
|
||||
- 2026-06-13: `Canvas::stroke_draw` live-pass sampler bind/unbind plus
|
||||
semantic texture-input dispatch now routes through retained stroke execution
|
||||
helpers; concrete GL object mapping, framebuffer ownership, shader timing,
|
||||
and final draw execution remain local to `Canvas`. Next slice should target
|
||||
the remaining live draw execution boundary inside `stroke_draw_samples()` or
|
||||
the final temporary-texture composite setup without reopening the landed
|
||||
dirty, face-framebuffer, pad, or texture-intent helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::stroke_draw_samples()` now routes
|
||||
destination bind/unbind, framebuffer copy callback wrapping, sample-point
|
||||
assembly, and brush-vertex upload/draw through one local helper; mixer-pass
|
||||
state execution and higher-level pass orchestration remain local to the
|
||||
preview node. Next slice should target the remaining mixer-pass state/copy
|
||||
ordering without reopening the landed preview live-pass, binding, or final
|
||||
composite helpers.
|
||||
- 2026-06-13: `Canvas::stroke_draw` main, pad, and dual live-pass
|
||||
texture-input binding/unbinding intent now routes through retained stroke
|
||||
execution helpers; sampler binding, concrete GL object mapping, framebuffer
|
||||
ownership, and final draw execution remain local to `Canvas`. Next slice
|
||||
should target the remaining sampler binding/teardown or the direct
|
||||
semantic-input-to-GL mapping callbacks without reopening the landed dirty,
|
||||
face-framebuffer, or pad helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` live-pass sampler
|
||||
binding, dual/main pass texture binding, checkerboard/background capture
|
||||
wrapping, and final preview copy-back now route through named local helpers;
|
||||
mixer state execution and per-sample GL ordering remain local to the preview
|
||||
node. Next slice should target the remaining mixer-pass state/copy ordering
|
||||
or sample-pass destination callback wrapping without reopening the landed
|
||||
preview live-pass or final-composite helpers.
|
||||
- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` dual-pass and
|
||||
main-pass frame-loop execution plus full-frame copy-back now route through
|
||||
shared local helpers; checkerboard/background capture ordering, texture-unit
|
||||
binding, mixer ownership, and final composite semantics remain local to the
|
||||
preview node. Next slice should target background/final-copy helpers or
|
||||
pass-level texture binding without reopening the new preview composite
|
||||
helper.
|
||||
- 2026-06-13: `Canvas::stroke_draw` main and dual live-pass per-face
|
||||
framebuffer begin/end execution plus pad-face array assembly now route
|
||||
through `legacy_canvas_stroke_execution_services.h`; shader activation
|
||||
timing, texture/sampler binding, framebuffer ownership, and final draw
|
||||
execution remain local to `Canvas`. Next slice should target the remaining
|
||||
pass-level texture binding or final composite setup without reopening the
|
||||
newer pad, dirty, or commit helpers.
|
||||
- 2026-06-13: `Canvas::stroke_draw` current and dual stroke per-face
|
||||
framebuffer/sample callback ordering now routes through
|
||||
`legacy_canvas_stroke_execution_services.h`; framebuffer ownership, shader
|
||||
uniform timing, sampler/texture binding, and draw execution remain local to
|
||||
Canvas. Next slice should target the remaining per-face retained state around
|
||||
sample execution without reopening the new pad-pass, dirty-mutation, or
|
||||
commit executors.
|
||||
- 2026-06-13: `Canvas::stroke_draw` current and dual stroke dirty-box mutation
|
||||
now routes through `legacy_canvas_stroke_execution_services.h`; framebuffer
|
||||
binding, shader uniform timing, sampler/texture binding, and draw execution
|
||||
remain local to Canvas. Next slice should wrap the retained per-face
|
||||
framebuffer/sample callback ordering without rewriting the new pad-pass or
|
||||
commit executors.
|
||||
- 2026-06-13: `Canvas::stroke_commit` now routes retained commit input
|
||||
texture/sampler binding, erase/composite draw dispatch, committed-copy, and
|
||||
dilate draw through `legacy_canvas_stroke_commit_services.h`; history
|
||||
readback, `ActionStroke` population, and layer dirty-box mutation remain
|
||||
local to Canvas.
|
||||
- 2026-06-13: `pp_paint_renderer` owns tested Canvas stroke commit sequencing
|
||||
and commit texture slot intent. Next slice should wire
|
||||
`legacy_canvas_stroke_commit_services.h` into `Canvas::stroke_commit` while
|
||||
keeping history/layer mutation local.
|
||||
|
||||
### LATER-004 - Remove Catch-All Platform Legacy Adapter
|
||||
|
||||
Status: Blocked
|
||||
Score: +5 platform and package parity
|
||||
Debt: `DEBT-0017`
|
||||
Blocked By: Apple/Web split tasks and per-platform package validation
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/platform_legacy/legacy_platform_services.*` is deleted or contains only
|
||||
compile-time unsupported stubs with debt entries.
|
||||
- Windows, Apple, Android, Linux, and Web platform services are named targets.
|
||||
- Platform-build and package-smoke report explicit pass/fail per platform.
|
||||
|
||||
## Completed Task Log
|
||||
|
||||
| Date | Task | Score Change | Validation | Commit |
|
||||
| --- | --- | ---: | --- | --- |
|
||||
| 2026-06-12 | RND-004 | +2 hardening and future backend readiness | `ctest --preset desktop-gpu --build-config Debug --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_renderer_gl\|pp_paint_renderer" --output-on-failure` | e37b2929 |
|
||||
| 2026-06-12 | DEP-002 | +1 build and CMake ownership | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | 648404ee |
|
||||
| 2026-06-12 | RND-002 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | RND-001 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | ADP-004 | +2 legacy adapter retirement | VS-bundled CMake build of `pp_app_core_app_dialog_tests` and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_app_dialog\|pano_cli_plan_app_dialog" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | PLT-001 | +2 platform and package parity | VS-bundled CMake build of `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; Apple remote build blocked by unpublished fmt submodule pointer before DEP-001 correction | 46fb8ef |
|
||||
| 2026-06-12 | RND-003 | +3 renderer boundary and OpenGL parity | VS-bundled CMake build of `pp_paint_renderer_compositor_tests`, `pp_app_core_document_export_tests`, and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | DEP-001 | +1 build and CMake ownership | VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | ADP-003 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_route\|pp_app_core_document_session\|pano_cli_plan_open_route\|pano_cli_simulate_app_session\|pano_cli_plan_document_file\|pano_cli_plan_document_version" --output-on-failure` | 34a9e910 |
|
||||
| 2026-06-12 | PLT-002 | +2 platform and package parity | `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | 8cd38401 |
|
||||
| 2026-06-12 | ADP-002 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer" --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | ae242852 |
|
||||
| 2026-06-12 | ADP-001 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_resize\|pp_app_core_document_canvas\|pano_cli_plan_document_resize\|pano_cli_plan_canvas_clear" --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core\|pano_cli_plan"` | e489b1e2 |
|
||||
| 2026-06-12 | MT-001 | 0 | `git diff -- docs\modernization\roadmap.md docs\modernization\tasks.md` | same docs slice |
|
||||
|
||||
## Task Template
|
||||
|
||||
Use this shape when adding a new task:
|
||||
|
||||
````markdown
|
||||
### AREA-000 - Imperative Task Name
|
||||
|
||||
Status: Ready
|
||||
Score: +N scorecard area
|
||||
Debt: `DEBT-0000`
|
||||
Scope: exact files or directories
|
||||
|
||||
Goal:
|
||||
|
||||
One paragraph describing the behavior or ownership change.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Binary, grep-able, or testable condition.
|
||||
- Debt log update condition.
|
||||
- Validation condition.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
command
|
||||
```
|
||||
````
|
||||
@@ -62,7 +62,7 @@ BOOL LoadWintab( void )
|
||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wacom_Tablet.dll" );
|
||||
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wintab32.dll" );
|
||||
LOG("calling LoadLibrary");
|
||||
ghWintab = LoadLibrary(L"Wintab32.dll");
|
||||
ghWintab = LoadLibraryW(L"Wintab32.dll");
|
||||
LOG("LoadLibrary called");
|
||||
|
||||
if ( !ghWintab )
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
project(panopainter)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(PanoPainterLinux LANGUAGES C CXX)
|
||||
|
||||
add_executable(panopainter
|
||||
src/main.cpp
|
||||
@@ -120,4 +118,7 @@ target_include_directories(panopainter PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(panopainter glfw curl GL dl X11 pthread)
|
||||
target_compile_features(panopainter PRIVATE cxx_std_23)
|
||||
set_target_properties(panopainter PROPERTIES
|
||||
CXX_EXTENSIONS OFF)
|
||||
target_compile_definitions(panopainter PUBLIC "$<$<CONFIG:DEBUG>:_DEBUG>")
|
||||
|
||||
65
scripts/automation/analyze.ps1
Normal file
65
scripts/automation/analyze.ps1
Normal file
@@ -0,0 +1,65 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[switch]$NoApp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @(
|
||||
"--preset", $Preset,
|
||||
"-DPP_ENABLE_MSVC_ANALYZE=ON",
|
||||
"-DPP_ENABLE_CLANG_TIDY=ON",
|
||||
"-DPP_ENABLE_CPPCHECK=ON"
|
||||
)
|
||||
if ($NoApp) {
|
||||
$argsList += "-DPP_BUILD_APP=OFF"
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
$shaderExitCode = 0
|
||||
$rendererBoundaryExitCode = 0
|
||||
|
||||
if ($configureExitCode -eq 0) {
|
||||
& cmake --build --preset $Preset --target panopainter_validate_shaders
|
||||
$shaderExitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
if ($configureExitCode -eq 0) {
|
||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check-renderer-boundary.ps1")
|
||||
$rendererBoundaryExitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
$exitCode = $configureExitCode
|
||||
if ($exitCode -eq 0 -and $shaderExitCode -ne 0) {
|
||||
$exitCode = $shaderExitCode
|
||||
}
|
||||
if ($exitCode -eq 0 -and $rendererBoundaryExitCode -ne 0) {
|
||||
$exitCode = $rendererBoundaryExitCode
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "analyze"
|
||||
preset = $Preset
|
||||
exitCode = $exitCode
|
||||
checks = @(
|
||||
[ordered]@{
|
||||
name = "configure"
|
||||
exitCode = $configureExitCode
|
||||
},
|
||||
[ordered]@{
|
||||
name = "shader-validation"
|
||||
exitCode = $shaderExitCode
|
||||
},
|
||||
[ordered]@{
|
||||
name = "renderer-boundary"
|
||||
exitCode = $rendererBoundaryExitCode
|
||||
}
|
||||
)
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress -Depth 4
|
||||
|
||||
exit $exitCode
|
||||
29
scripts/automation/analyze.sh
Normal file
29
scripts/automation/analyze.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
preset="${1:-linux-clang}"
|
||||
start="$(date +%s)"
|
||||
cmake --preset "$preset" -DPP_ENABLE_CLANG_TIDY=ON -DPP_ENABLE_CPPCHECK=ON
|
||||
configure_exit_code="$?"
|
||||
shader_exit_code="0"
|
||||
renderer_boundary_exit_code="0"
|
||||
if [ "$configure_exit_code" -eq 0 ]; then
|
||||
cmake --build --preset "$preset" --target panopainter_validate_shaders
|
||||
shader_exit_code="$?"
|
||||
fi
|
||||
if [ "$configure_exit_code" -eq 0 ]; then
|
||||
"$script_dir/check-renderer-boundary.sh"
|
||||
renderer_boundary_exit_code="$?"
|
||||
fi
|
||||
exit_code="$configure_exit_code"
|
||||
if [ "$exit_code" -eq 0 ] && [ "$shader_exit_code" -ne 0 ]; then
|
||||
exit_code="$shader_exit_code"
|
||||
fi
|
||||
if [ "$exit_code" -eq 0 ] && [ "$renderer_boundary_exit_code" -ne 0 ]; then
|
||||
exit_code="$renderer_boundary_exit_code"
|
||||
fi
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s},{"name":"renderer-boundary","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$renderer_boundary_exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
103
scripts/automation/android-legacy-package-build.ps1
Normal file
103
scripts/automation/android-legacy-package-build.ps1
Normal file
@@ -0,0 +1,103 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Packages = @("standard"),
|
||||
[switch]$ConfigureOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\android-sdk-env.ps1"
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
$Packages = @(Expand-ArgumentList -Values $Packages)
|
||||
|
||||
$toolchain = Set-AndroidSdkToolchainEnvironment
|
||||
$packageMap = @{
|
||||
standard = "android/android"
|
||||
quest = "android/quest"
|
||||
focus = "android/focus"
|
||||
}
|
||||
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($package in $Packages) {
|
||||
if (!$packageMap.ContainsKey($package)) {
|
||||
throw "Unknown Android package '$package'. Expected one of: standard, quest, focus."
|
||||
}
|
||||
|
||||
$sourceDir = $packageMap[$package]
|
||||
$buildDir = "out/build/android-legacy-$package-arm64"
|
||||
$toolchainFile = Join-Path $toolchain.ndkPath "build\cmake\android.toolchain.cmake"
|
||||
|
||||
$configureArgs = @(
|
||||
"-S", $sourceDir,
|
||||
"-B", $buildDir,
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$toolchainFile",
|
||||
"-DANDROID_ABI=arm64-v8a",
|
||||
"-DANDROID_PLATFORM=android-23"
|
||||
)
|
||||
|
||||
& $toolchain.cmakeCommand @configureArgs
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
if ($configureExitCode -ne 0) {
|
||||
if ($overallExitCode -eq 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
}
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "configure"
|
||||
exitCode = $configureExitCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ($ConfigureOnly) {
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "configure"
|
||||
exitCode = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
& $toolchain.cmakeCommand --build $buildDir --target native-lib
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "build"
|
||||
target = "native-lib"
|
||||
exitCode = $buildExitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "android-legacy-package-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidToolchain = $toolchain
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
exit $overallExitCode
|
||||
212
scripts/automation/android-sdk-env.ps1
Normal file
212
scripts/automation/android-sdk-env.ps1
Normal file
@@ -0,0 +1,212 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-AndroidSdkRoot {
|
||||
$candidates = @(
|
||||
$env:ANDROID_SDK_ROOT,
|
||||
$env:ANDROID_HOME,
|
||||
(Join-Path $env:LOCALAPPDATA "Android\Sdk")
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Container)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
throw "Android SDK root was not found. Install command-line tools or set ANDROID_SDK_ROOT."
|
||||
}
|
||||
|
||||
function Get-LatestAndroidSdkPackageDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName
|
||||
)
|
||||
|
||||
$packageRoot = Join-Path $SdkRoot $PackageName
|
||||
if (!(Test-Path -LiteralPath $packageRoot -PathType Container)) {
|
||||
throw "Android SDK package directory not found: $packageRoot"
|
||||
}
|
||||
|
||||
$packages = @(Get-ChildItem -LiteralPath $packageRoot -Directory |
|
||||
Where-Object { $_.Name -match '^\d+(\.\d+)*$' } |
|
||||
Sort-Object { [version]$_.Name } -Descending)
|
||||
|
||||
if ($packages.Count -eq 0) {
|
||||
throw "No installed Android SDK package versions found under $packageRoot"
|
||||
}
|
||||
|
||||
return $packages[0]
|
||||
}
|
||||
|
||||
function Get-AndroidSdkManagerCommand {
|
||||
param([Parameter(Mandatory=$true)][string]$SdkRoot)
|
||||
|
||||
$candidates = @(
|
||||
(Join-Path $SdkRoot "cmdline-tools\latest\bin\sdkmanager.bat"),
|
||||
(Join-Path $SdkRoot "cmdline-tools\latest\bin\sdkmanager.exe"),
|
||||
(Join-Path $SdkRoot "tools\bin\sdkmanager.bat"),
|
||||
(Join-Path $SdkRoot "tools\bin\sdkmanager.exe")
|
||||
)
|
||||
|
||||
$cmdlineToolsRoot = Join-Path $SdkRoot "cmdline-tools"
|
||||
if (Test-Path -LiteralPath $cmdlineToolsRoot -PathType Container) {
|
||||
$toolVersions = @(Get-ChildItem -LiteralPath $cmdlineToolsRoot -Directory |
|
||||
Where-Object { $_.Name -ne "latest" } |
|
||||
Sort-Object {
|
||||
try { [version]$_.Name } catch { [version]"0.0" }
|
||||
} -Descending)
|
||||
|
||||
foreach ($toolVersion in $toolVersions) {
|
||||
$candidates += (Join-Path $toolVersion.FullName "bin\sdkmanager.bat")
|
||||
$candidates += (Join-Path $toolVersion.FullName "bin\sdkmanager.exe")
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
$pathCommand = Get-Command "sdkmanager" -ErrorAction SilentlyContinue
|
||||
if ($pathCommand) {
|
||||
return $pathCommand.Source
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-LatestAvailableAndroidSdkPackageVersion {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$SdkManagerCommand,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName
|
||||
)
|
||||
|
||||
$output = @(& $SdkManagerCommand "--sdk_root=$SdkRoot" "--list" 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
throw "sdkmanager --list failed while checking $PackageName packages: $($output -join [Environment]::NewLine)"
|
||||
}
|
||||
|
||||
$versions = @()
|
||||
$pattern = "^\s*$([regex]::Escape($PackageName));([0-9]+(?:\.[0-9]+)*)\s*\|"
|
||||
foreach ($line in $output) {
|
||||
$text = $line.ToString()
|
||||
if ($text -match $pattern) {
|
||||
$versions += $Matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ($versions.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return @($versions | Sort-Object { [version]$_ } -Descending | Select-Object -First 1)[0]
|
||||
}
|
||||
|
||||
function Install-AndroidSdkPackage {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$SdkManagerCommand,
|
||||
[Parameter(Mandatory=$true)][string]$PackageId
|
||||
)
|
||||
|
||||
$licenseInput = ("y`n" * 100)
|
||||
$output = @($licenseInput | & $SdkManagerCommand "--sdk_root=$SdkRoot" "--install" $PackageId 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
throw "sdkmanager failed to install $PackageId with exit code ${exitCode}: $($output -join [Environment]::NewLine)"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-LatestAndroidSdkPackageDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName,
|
||||
[string]$SdkManagerCommand
|
||||
)
|
||||
|
||||
$installedBefore = $null
|
||||
try {
|
||||
$installedBefore = Get-LatestAndroidSdkPackageDirectory -SdkRoot $SdkRoot -PackageName $PackageName
|
||||
} catch {
|
||||
$installedBefore = $null
|
||||
}
|
||||
|
||||
$availableVersion = $null
|
||||
$action = "using-installed"
|
||||
if ($SdkManagerCommand) {
|
||||
$availableVersion = Get-LatestAvailableAndroidSdkPackageVersion `
|
||||
-SdkRoot $SdkRoot `
|
||||
-SdkManagerCommand $SdkManagerCommand `
|
||||
-PackageName $PackageName
|
||||
|
||||
$installedVersion = if ($installedBefore) { [version]$installedBefore.Name } else { $null }
|
||||
$availableParsed = if ($availableVersion) { [version]$availableVersion } else { $null }
|
||||
if ($availableParsed -and (!$installedVersion -or $availableParsed -gt $installedVersion)) {
|
||||
Install-AndroidSdkPackage `
|
||||
-SdkRoot $SdkRoot `
|
||||
-SdkManagerCommand $SdkManagerCommand `
|
||||
-PackageId "$PackageName;$availableVersion"
|
||||
$action = "installed-latest-available"
|
||||
} elseif ($availableParsed) {
|
||||
$action = "already-latest-available"
|
||||
} else {
|
||||
$action = "available-version-not-listed"
|
||||
}
|
||||
} elseif (!$installedBefore) {
|
||||
throw "No installed Android SDK package versions found under $(Join-Path $SdkRoot $PackageName), and sdkmanager was not found."
|
||||
}
|
||||
|
||||
$selected = Get-LatestAndroidSdkPackageDirectory -SdkRoot $SdkRoot -PackageName $PackageName
|
||||
return [ordered]@{
|
||||
directory = $selected
|
||||
update = [ordered]@{
|
||||
package = $PackageName
|
||||
installedVersionBefore = if ($installedBefore) { $installedBefore.Name } else { $null }
|
||||
availableVersion = $availableVersion
|
||||
selectedVersion = $selected.Name
|
||||
action = $action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Set-AndroidSdkToolchainEnvironment {
|
||||
$sdkRoot = Get-AndroidSdkRoot
|
||||
$sdkManagerCommand = Get-AndroidSdkManagerCommand -SdkRoot $sdkRoot
|
||||
$ndkSelection = Ensure-LatestAndroidSdkPackageDirectory `
|
||||
-SdkRoot $sdkRoot `
|
||||
-PackageName "ndk" `
|
||||
-SdkManagerCommand $sdkManagerCommand
|
||||
$cmakeSelection = Ensure-LatestAndroidSdkPackageDirectory `
|
||||
-SdkRoot $sdkRoot `
|
||||
-PackageName "cmake" `
|
||||
-SdkManagerCommand $sdkManagerCommand
|
||||
|
||||
$ndk = $ndkSelection.directory
|
||||
$cmake = $cmakeSelection.directory
|
||||
$cmakeCommand = Join-Path $cmake.FullName "bin\cmake.exe"
|
||||
|
||||
if (!(Test-Path -LiteralPath $cmakeCommand -PathType Leaf)) {
|
||||
throw "Android SDK CMake executable not found: $cmakeCommand"
|
||||
}
|
||||
|
||||
$env:ANDROID_HOME = $sdkRoot
|
||||
$env:ANDROID_SDK_ROOT = $sdkRoot
|
||||
$env:ANDROID_NDK_HOME = $ndk.FullName
|
||||
$env:ANDROID_NDK_ROOT = $ndk.FullName
|
||||
|
||||
return [ordered]@{
|
||||
sdkRoot = $sdkRoot
|
||||
sdkManagerCommand = $sdkManagerCommand
|
||||
packageUpdates = @($ndkSelection.update, $cmakeSelection.update)
|
||||
ndkVersion = $ndk.Name
|
||||
ndkPath = $ndk.FullName
|
||||
cmakeVersion = $cmake.Name
|
||||
cmakeCommand = $cmakeCommand
|
||||
}
|
||||
}
|
||||
201
scripts/automation/android-sdk-env.sh
Normal file
201
scripts/automation/android-sdk-env.sh
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
android_sdk_root() {
|
||||
if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT" ]; then
|
||||
printf '%s\n' "$ANDROID_SDK_ROOT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${ANDROID_HOME:-}" ] && [ -d "$ANDROID_HOME" ]; then
|
||||
printf '%s\n' "$ANDROID_HOME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${LOCALAPPDATA:-}" ]; then
|
||||
local_sdk="$LOCALAPPDATA/Android/Sdk"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
local_sdk="$(cygpath -u "$local_sdk")"
|
||||
fi
|
||||
if [ -d "$local_sdk" ]; then
|
||||
printf '%s\n' "$local_sdk"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d "$HOME/Android/Sdk" ]; then
|
||||
printf '%s\n' "$HOME/Android/Sdk"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
latest_android_package_dir() {
|
||||
root="$1"
|
||||
package="$2"
|
||||
package_root="$root/$package"
|
||||
[ -d "$package_root" ] || return 1
|
||||
latest="$(
|
||||
for dir in "$package_root"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
version="${dir##*/}"
|
||||
printf '%s\n' "$version"
|
||||
done | grep -E '^[0-9]+(\.[0-9]+)*$' | sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -n 1
|
||||
)"
|
||||
[ -n "$latest" ] || return 1
|
||||
printf '%s/%s\n' "$package_root" "$latest"
|
||||
}
|
||||
|
||||
android_sdkmanager_command() {
|
||||
root="$1"
|
||||
for candidate in \
|
||||
"$root/cmdline-tools/latest/bin/sdkmanager" \
|
||||
"$root/cmdline-tools/latest/bin/sdkmanager.bat" \
|
||||
"$root/tools/bin/sdkmanager" \
|
||||
"$root/tools/bin/sdkmanager.bat"
|
||||
do
|
||||
if [ -x "$candidate" ] || [ -f "$candidate" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -d "$root/cmdline-tools" ]; then
|
||||
for version in "$root/cmdline-tools"/*; do
|
||||
[ -d "$version" ] || continue
|
||||
[ "${version##*/}" = "latest" ] && continue
|
||||
for candidate in "$version/bin/sdkmanager" "$version/bin/sdkmanager.bat"; do
|
||||
if [ -x "$candidate" ] || [ -f "$candidate" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v sdkmanager >/dev/null 2>&1; then
|
||||
command -v sdkmanager
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
latest_version_from_stdin() {
|
||||
sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n -k 5,5n | tail -n 1
|
||||
}
|
||||
|
||||
latest_available_android_package_version() {
|
||||
root="$1"
|
||||
sdkmanager_cmd="$2"
|
||||
package="$3"
|
||||
output="$("$sdkmanager_cmd" "--sdk_root=$root" --list)" || return 1
|
||||
printf '%s\n' "$output" |
|
||||
sed -n "s/^[[:space:]]*$package;\\([0-9][0-9.]*\\)[[:space:]]*|.*/\\1/p" |
|
||||
latest_version_from_stdin
|
||||
}
|
||||
|
||||
accept_android_sdk_licenses() {
|
||||
i=0
|
||||
while [ "$i" -lt 100 ]; do
|
||||
printf '%s\n' "y"
|
||||
i="$((i + 1))"
|
||||
done
|
||||
}
|
||||
|
||||
install_android_sdk_package() {
|
||||
root="$1"
|
||||
sdkmanager_cmd="$2"
|
||||
package_id="$3"
|
||||
accept_android_sdk_licenses | "$sdkmanager_cmd" "--sdk_root=$root" --install "$package_id" >&2
|
||||
}
|
||||
|
||||
record_android_package_update() {
|
||||
package="$1"
|
||||
installed_before="$2"
|
||||
available="$3"
|
||||
selected="$4"
|
||||
action="$5"
|
||||
|
||||
case "$package" in
|
||||
ndk)
|
||||
ANDROID_NDK_INSTALLED_BEFORE="$installed_before"
|
||||
ANDROID_NDK_AVAILABLE_VERSION="$available"
|
||||
ANDROID_NDK_UPDATE_ACTION="$action"
|
||||
;;
|
||||
cmake)
|
||||
ANDROID_CMAKE_INSTALLED_BEFORE="$installed_before"
|
||||
ANDROID_CMAKE_AVAILABLE_VERSION="$available"
|
||||
ANDROID_CMAKE_UPDATE_ACTION="$action"
|
||||
;;
|
||||
esac
|
||||
export ANDROID_NDK_INSTALLED_BEFORE ANDROID_NDK_AVAILABLE_VERSION ANDROID_NDK_UPDATE_ACTION
|
||||
export ANDROID_CMAKE_INSTALLED_BEFORE ANDROID_CMAKE_AVAILABLE_VERSION ANDROID_CMAKE_UPDATE_ACTION
|
||||
printf '%s\n' "$selected" >/dev/null
|
||||
}
|
||||
|
||||
ensure_latest_android_package_dir() {
|
||||
root="$1"
|
||||
package="$2"
|
||||
sdkmanager_cmd="${3:-}"
|
||||
|
||||
installed_dir="$(latest_android_package_dir "$root" "$package" 2>/dev/null || true)"
|
||||
installed_before="${installed_dir##*/}"
|
||||
[ -n "$installed_dir" ] || installed_before=""
|
||||
available_version=""
|
||||
action="using-installed"
|
||||
|
||||
if [ -n "$sdkmanager_cmd" ]; then
|
||||
available_version="$(latest_available_android_package_version "$root" "$sdkmanager_cmd" "$package")" || return 1
|
||||
if [ -n "$available_version" ]; then
|
||||
if [ -z "$installed_before" ]; then
|
||||
install_android_sdk_package "$root" "$sdkmanager_cmd" "$package;$available_version" || return 1
|
||||
action="installed-latest-available"
|
||||
else
|
||||
newest="$(printf '%s\n%s\n' "$installed_before" "$available_version" | latest_version_from_stdin)"
|
||||
if [ "$newest" = "$available_version" ] && [ "$available_version" != "$installed_before" ]; then
|
||||
install_android_sdk_package "$root" "$sdkmanager_cmd" "$package;$available_version" || return 1
|
||||
action="installed-latest-available"
|
||||
else
|
||||
action="already-latest-available"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
action="available-version-not-listed"
|
||||
fi
|
||||
elif [ -z "$installed_dir" ]; then
|
||||
printf '%s\n' "No installed Android SDK package was found under $root/$package, and sdkmanager was not found." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
selected_dir="$(latest_android_package_dir "$root" "$package")" || return 1
|
||||
record_android_package_update "$package" "$installed_before" "$available_version" "${selected_dir##*/}" "$action"
|
||||
printf '%s\n' "$selected_dir"
|
||||
}
|
||||
|
||||
set_android_sdk_toolchain_environment() {
|
||||
sdk_root="$(android_sdk_root)" || {
|
||||
printf '%s\n' "Android SDK root was not found. Install command-line tools or set ANDROID_SDK_ROOT." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
sdkmanager_cmd="$(android_sdkmanager_command "$sdk_root" || true)"
|
||||
|
||||
ndk_dir="$(ensure_latest_android_package_dir "$sdk_root" ndk "$sdkmanager_cmd")" || return 1
|
||||
cmake_dir="$(ensure_latest_android_package_dir "$sdk_root" cmake "$sdkmanager_cmd")" || return 1
|
||||
|
||||
cmake_command="$cmake_dir/bin/cmake"
|
||||
[ -x "$cmake_command" ] || {
|
||||
printf '%s\n' "Android SDK CMake executable not found: $cmake_command" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
export ANDROID_HOME="$sdk_root"
|
||||
export ANDROID_SDK_ROOT="$sdk_root"
|
||||
export ANDROID_NDK_HOME="$ndk_dir"
|
||||
export ANDROID_NDK_ROOT="$ndk_dir"
|
||||
export ANDROID_CMAKE_COMMAND="$cmake_command"
|
||||
export ANDROID_NDK_VERSION="${ndk_dir##*/}"
|
||||
export ANDROID_CMAKE_VERSION="${cmake_dir##*/}"
|
||||
export ANDROID_SDKMANAGER_COMMAND="$sdkmanager_cmd"
|
||||
}
|
||||
93
scripts/automation/apple-remote-build.ps1
Normal file
93
scripts/automation/apple-remote-build.ps1
Normal file
@@ -0,0 +1,93 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$HostName = "panopainter-mac",
|
||||
[string]$RemoteDirectory = "~/Dev/panopainter",
|
||||
[string]$RepositoryUrl = "ssh://git@git.omar.synology.me:3022/omar/panopainter.git",
|
||||
[string]$Branch = "codex/modernization-cmake-foundation",
|
||||
[string[]]$Presets = @("macos", "ios-simulator", "ios-device")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Join-RemoteArgument {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return ($expanded -join " ")
|
||||
}
|
||||
|
||||
function ConvertTo-ShellSingleQuoted {
|
||||
param([string]$Value)
|
||||
|
||||
return "'" + ($Value -replace "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
$presetArgument = Join-RemoteArgument -Values $Presets
|
||||
$remoteDirectoryLiteral = ConvertTo-ShellSingleQuoted -Value $RemoteDirectory
|
||||
$repositoryLiteral = ConvertTo-ShellSingleQuoted -Value $RepositoryUrl
|
||||
$branchLiteral = ConvertTo-ShellSingleQuoted -Value $Branch
|
||||
$presetLiteral = ConvertTo-ShellSingleQuoted -Value $presetArgument
|
||||
|
||||
$remoteScript = @"
|
||||
set -eu
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:`$HOME/tools/bin:`$PATH"
|
||||
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
|
||||
remote_dir=$remoteDirectoryLiteral
|
||||
repository_url=$repositoryLiteral
|
||||
branch_name=$branchLiteral
|
||||
presets=$presetLiteral
|
||||
|
||||
case "`$remote_dir" in
|
||||
"~/"*) remote_dir="`$HOME/`$(printf '%s' "`$remote_dir" | sed 's|^~/||')" ;;
|
||||
esac
|
||||
|
||||
mkdir -p "`$(dirname "`$remote_dir")"
|
||||
if [ ! -d "`$remote_dir/.git" ]; then
|
||||
git clone "`$repository_url" "`$remote_dir"
|
||||
fi
|
||||
|
||||
cd "`$remote_dir"
|
||||
git fetch origin
|
||||
git checkout "`$branch_name"
|
||||
git pull --ff-only origin "`$branch_name"
|
||||
|
||||
git submodule update --init --recursive \
|
||||
libs/tinyxml2 \
|
||||
libs/glm \
|
||||
libs/stb/stb \
|
||||
libs/yoga \
|
||||
libs/poly2tri \
|
||||
libs/base64 \
|
||||
libs/sqlite3 \
|
||||
libs/nanort \
|
||||
libs/hash-library \
|
||||
libs/fmt \
|
||||
libs/glad \
|
||||
libs/tinyfiledialogs
|
||||
|
||||
mkdir -p out/logs
|
||||
log="out/logs/apple-platform-build-`$(date +%Y%m%d-%H%M%S).log"
|
||||
set +e
|
||||
sh ./scripts/automation/platform-build.sh "`$presets" > "`$log" 2>&1
|
||||
exit_code=`$?
|
||||
set -e
|
||||
|
||||
printf '{"command":"apple-remote-build","host":"%s","branch":"%s","presets":"%s","log":"%s","exitCode":%s}\n' \
|
||||
"`$(hostname)" "`$branch_name" "`$presets" "`$log" "`$exit_code"
|
||||
tail -n 80 "`$log"
|
||||
exit "`$exit_code"
|
||||
"@
|
||||
|
||||
$remoteScript = $remoteScript -replace "`r`n", "`n"
|
||||
$encodedRemoteScript = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($remoteScript))
|
||||
& ssh -o BatchMode=yes $HostName "printf '%s' '$encodedRemoteScript' | base64 -D | sh"
|
||||
exit $LASTEXITCODE
|
||||
28
scripts/automation/build.ps1
Normal file
28
scripts/automation/build.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @("--build", "--preset", $Preset, "--config", $Configuration)
|
||||
if ($Target.Length -gt 0) {
|
||||
$argsList += @("--target", $Target)
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "build"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
17
scripts/automation/build.sh
Normal file
17
scripts/automation/build.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-}"
|
||||
start="$(date +%s)"
|
||||
if [ -n "$target" ]; then
|
||||
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
else
|
||||
cmake --build --preset "$preset" --config "$configuration"
|
||||
fi
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"build","preset":"%s","configuration":"%s","target":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
62
scripts/automation/check-renderer-boundary.ps1
Normal file
62
scripts/automation/check-renderer-boundary.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Root = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($Root)) {
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
}
|
||||
$started = Get-Date
|
||||
$pattern = '\b(?:GL|WGL)_[A-Z0-9_]+\b'
|
||||
$allowed = @(
|
||||
"src/renderer_gl/",
|
||||
"src/rtt.cpp",
|
||||
"src/texture.cpp"
|
||||
)
|
||||
$violations = @()
|
||||
$files = Get-ChildItem -Path (Join-Path $Root "src") -Recurse -File -Include *.c,*.cc,*.cpp,*.h,*.hpp
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/').Replace('\', '/')
|
||||
$isAllowed = $false
|
||||
foreach ($prefix in $allowed) {
|
||||
if ($relative.StartsWith($prefix)) {
|
||||
$isAllowed = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($isAllowed) {
|
||||
continue
|
||||
}
|
||||
|
||||
$lineNumber = 0
|
||||
foreach ($line in Get-Content -LiteralPath $file.FullName) {
|
||||
$lineNumber += 1
|
||||
$trimmed = $line.TrimStart()
|
||||
if ($trimmed.StartsWith("//")) {
|
||||
continue
|
||||
}
|
||||
$matches = [regex]::Matches($line, $pattern)
|
||||
foreach ($match in $matches) {
|
||||
$violations += [ordered]@{
|
||||
file = $relative
|
||||
line = $lineNumber
|
||||
token = $match.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$exitCode = if ($violations.Count -eq 0) { 0 } else { 1 }
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "check-renderer-boundary"
|
||||
exitCode = $exitCode
|
||||
violationCount = $violations.Count
|
||||
violations = @($violations | Select-Object -First 50)
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress -Depth 4
|
||||
|
||||
exit $exitCode
|
||||
34
scripts/automation/check-renderer-boundary.sh
Normal file
34
scripts/automation/check-renderer-boundary.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
root="${1:-$(CDPATH= cd -- "$script_dir/../.." && pwd)}"
|
||||
start="$(date +%s)"
|
||||
tmp="${TMPDIR:-/tmp}/panopainter-renderer-boundary-$$.txt"
|
||||
|
||||
find "$root/src" -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) | while IFS= read -r file; do
|
||||
rel="${file#"$root"/}"
|
||||
case "$rel" in
|
||||
src/renderer_gl/*|src/rtt.cpp|src/texture.cpp)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
awk -v rel="$rel" '
|
||||
/^[[:space:]]*\/\// { next }
|
||||
match($0, /\<(GL|WGL)_[A-Z0-9_]+\>/) {
|
||||
print rel ":" FNR ":" substr($0, RSTART, RLENGTH)
|
||||
}
|
||||
' "$file"
|
||||
done > "$tmp"
|
||||
|
||||
count="$(wc -l < "$tmp" | tr -d '[:space:]')"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
exit_code="0"
|
||||
if [ "$count" -ne 0 ]; then
|
||||
exit_code="1"
|
||||
fi
|
||||
|
||||
printf '{"command":"check-renderer-boundary","exitCode":%s,"violationCount":%s,"elapsedMs":%s}\n' "$exit_code" "$count" "$elapsed_ms"
|
||||
rm -f "$tmp"
|
||||
exit "$exit_code"
|
||||
25
scripts/automation/configure.ps1
Normal file
25
scripts/automation/configure.ps1
Normal file
@@ -0,0 +1,25 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[switch]$NoApp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$argsList = @("--preset", $Preset)
|
||||
if ($NoApp) {
|
||||
$argsList += "-DPP_BUILD_APP=OFF"
|
||||
}
|
||||
|
||||
& cmake @argsList
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "configure"
|
||||
preset = $Preset
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
11
scripts/automation/configure.sh
Normal file
11
scripts/automation/configure.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
start="$(date +%s)"
|
||||
cmake --preset "$preset"
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"configure","preset":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
413
scripts/automation/package-smoke.ps1
Normal file
413
scripts/automation/package-smoke.ps1
Normal file
@@ -0,0 +1,413 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = "PanoPainter",
|
||||
[string]$CMakeCommand = "cmake",
|
||||
[switch]$ReadinessOnly,
|
||||
[switch]$AndroidNativeChecks,
|
||||
[string[]]$PackageKinds = @(
|
||||
"windows-appx",
|
||||
"android-standard-apk",
|
||||
"android-quest-apk",
|
||||
"android-focus-apk",
|
||||
"apple-bundle",
|
||||
"linux-app",
|
||||
"webgl"
|
||||
)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$root = (Get-Location).Path
|
||||
|
||||
function Test-CommandAvailable {
|
||||
param([string]$Name)
|
||||
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
function New-ArtifactCheck {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Path,
|
||||
[string]$PathType = "Any"
|
||||
)
|
||||
|
||||
$exists = if ($PathType -eq "Container") {
|
||||
Test-Path -LiteralPath $Path -PathType Container
|
||||
} elseif ($PathType -eq "Leaf") {
|
||||
Test-Path -LiteralPath $Path -PathType Leaf
|
||||
} else {
|
||||
Test-Path -LiteralPath $Path
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
name = $Name
|
||||
path = $Path
|
||||
pathType = $PathType
|
||||
exists = $exists
|
||||
}
|
||||
}
|
||||
|
||||
function New-Prerequisite {
|
||||
param(
|
||||
[string]$Name,
|
||||
[bool]$Available,
|
||||
[string]$Detail
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
name = $Name
|
||||
available = $Available
|
||||
detail = $Detail
|
||||
}
|
||||
}
|
||||
|
||||
function New-PackageReadiness {
|
||||
param(
|
||||
[string]$Kind,
|
||||
[string]$Status,
|
||||
[string]$Reason,
|
||||
[object[]]$Prerequisites,
|
||||
[object[]]$Artifacts,
|
||||
[string]$ValidationCommand
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
kind = $Kind
|
||||
status = $Status
|
||||
reason = $Reason
|
||||
debt = "DEBT-0011"
|
||||
validationCommand = $ValidationCommand
|
||||
prerequisites = $Prerequisites
|
||||
artifacts = $Artifacts
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AndroidNativeCheckPlan {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$packages = @()
|
||||
if ($Kinds -contains "android-standard-apk") {
|
||||
$packages += [ordered]@{
|
||||
packages = @("standard")
|
||||
configureOnly = $false
|
||||
}
|
||||
}
|
||||
|
||||
$configureOnlyPackages = @()
|
||||
if ($Kinds -contains "android-quest-apk") {
|
||||
$configureOnlyPackages += "quest"
|
||||
}
|
||||
if ($Kinds -contains "android-focus-apk") {
|
||||
$configureOnlyPackages += "focus"
|
||||
}
|
||||
if ($configureOnlyPackages.Count -gt 0) {
|
||||
$packages += [ordered]@{
|
||||
packages = $configureOnlyPackages
|
||||
configureOnly = $true
|
||||
}
|
||||
}
|
||||
|
||||
return $packages
|
||||
}
|
||||
|
||||
function Invoke-AndroidNativePackageChecks {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$plans = @(Get-AndroidNativeCheckPlan -Kinds $Kinds)
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($plan in $plans) {
|
||||
$arguments = @(
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Join-Path $root "scripts/automation/android-legacy-package-build.ps1"),
|
||||
"-Packages", ($plan.packages -join ",")
|
||||
)
|
||||
if ($plan.configureOnly) {
|
||||
$arguments += "-ConfigureOnly"
|
||||
}
|
||||
|
||||
$output = @(& powershell @arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $exitCode
|
||||
}
|
||||
|
||||
$jsonLine = @($output | ForEach-Object { $_.ToString() } | Where-Object { $_.TrimStart().StartsWith("{") } | Select-Object -Last 1)
|
||||
$summary = $null
|
||||
if ($jsonLine.Count -gt 0) {
|
||||
try {
|
||||
$summary = $jsonLine[-1] | ConvertFrom-Json
|
||||
} catch {
|
||||
$summary = $null
|
||||
}
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
packages = $plan.packages
|
||||
configureOnly = [bool]$plan.configureOnly
|
||||
exitCode = $exitCode
|
||||
command = "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages $($plan.packages -join ',')$(if ($plan.configureOnly) { ' -ConfigureOnly' } else { '' })"
|
||||
summary = $summary
|
||||
}
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
requested = $plans.Count -gt 0
|
||||
exitCode = $overallExitCode
|
||||
results = $results
|
||||
}
|
||||
}
|
||||
|
||||
$PackageKinds = @(Expand-ArgumentList -Values $PackageKinds)
|
||||
|
||||
function Get-PackageReadiness {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$readiness = @()
|
||||
foreach ($kind in $Kinds) {
|
||||
switch ($kind) {
|
||||
"windows-appx" {
|
||||
$wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||
$manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest"
|
||||
$appPackages = Join-Path $root "PanoPainterPackage/AppPackages"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
|
||||
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
|
||||
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
|
||||
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-standard-apk" {
|
||||
$gradle = Join-Path $root "android/android/build.gradle"
|
||||
$manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/android/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/android assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-quest-apk" {
|
||||
$gradle = Join-Path $root "android/quest/build.gradle"
|
||||
$manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/quest/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/quest assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-focus-apk" {
|
||||
$gradle = Join-Path $root "android/focus/build.gradle"
|
||||
$manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/focus/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/focus assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"apple-bundle" {
|
||||
$xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj"
|
||||
$bundleDir = Join-Path $root "out/package/apple"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
|
||||
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
|
||||
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"linux-app" {
|
||||
$linuxCmake = Join-Path $root "linux/CMakeLists.txt"
|
||||
$linuxBinary = Join-Path $root "out/package/linux/panopainter"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "retained-linux-cmake-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "retained-linux-cmake" -Available (Test-Path -LiteralPath $linuxCmake -PathType Leaf) -Detail $linuxCmake),
|
||||
(New-Prerequisite -Name "cmake" -Available (Test-CommandAvailable "cmake") -Detail "Linux retained app CMake configure/build tool"),
|
||||
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "linux-clang"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "linux-app-output" -Path $linuxBinary -PathType "Leaf")
|
||||
)
|
||||
}
|
||||
"webgl" {
|
||||
$webglCmake = Join-Path $root "webgl/CMakeLists.txt"
|
||||
$webDir = Join-Path $root "out/package/webgl"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "retained-webgl-cmake-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "retained-webgl-cmake" -Available (Test-Path -LiteralPath $webglCmake -PathType Leaf) -Detail $webglCmake),
|
||||
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
|
||||
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
|
||||
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $readiness
|
||||
}
|
||||
|
||||
$androidNativeValidation = if ($AndroidNativeChecks) {
|
||||
Invoke-AndroidNativePackageChecks -Kinds $PackageKinds
|
||||
} else {
|
||||
[ordered]@{
|
||||
requested = $false
|
||||
exitCode = 0
|
||||
results = @()
|
||||
}
|
||||
}
|
||||
|
||||
if ($ReadinessOnly) {
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "readiness"
|
||||
exitCode = 0
|
||||
elapsedMs = $elapsed
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
exit $androidNativeValidation.exitCode
|
||||
}
|
||||
|
||||
& $CMakeCommand --build --preset $Preset --config $Configuration --target $Target
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0) {
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "build"
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $buildExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
exit $buildExitCode
|
||||
}
|
||||
|
||||
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
|
||||
$targetDir = Split-Path -Parent $binaryDir
|
||||
$dataDir = Join-Path $targetDir "data"
|
||||
$curlDll = if ($Configuration -eq "Debug") { "libcurl_debug.dll" } else { "libcurl.dll" }
|
||||
$checks = @(
|
||||
[ordered]@{ name = "executable"; path = $binaryDir; exists = Test-Path -LiteralPath $binaryDir -PathType Leaf },
|
||||
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container },
|
||||
[ordered]@{ name = "BugTrapU-x64.dll"; path = (Join-Path $targetDir "BugTrapU-x64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "BugTrapU-x64.dll") -PathType Leaf },
|
||||
[ordered]@{ name = $curlDll; path = (Join-Path $targetDir $curlDll); exists = Test-Path -LiteralPath (Join-Path $targetDir $curlDll) -PathType Leaf },
|
||||
[ordered]@{ name = "libyuv.dll"; path = (Join-Path $targetDir "libyuv.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libyuv.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "libmp4v2.dll"; path = (Join-Path $targetDir "libmp4v2.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libmp4v2.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "openh264-2.0.0-win64.dll"; path = (Join-Path $targetDir "openh264-2.0.0-win64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openh264-2.0.0-win64.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "openvr_api.dll"; path = (Join-Path $targetDir "openvr_api.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openvr_api.dll") -PathType Leaf }
|
||||
)
|
||||
|
||||
$failed = @($checks | Where-Object { -not $_.exists })
|
||||
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
|
||||
if ($androidNativeValidation.exitCode -ne 0 -and $exitCode -eq 0) {
|
||||
$exitCode = $androidNativeValidation.exitCode
|
||||
}
|
||||
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsedMs
|
||||
checks = $checks
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
|
||||
exit $exitCode
|
||||
132
scripts/automation/package-smoke.sh
Normal file
132
scripts/automation/package-smoke.sh
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-PanoPainter}"
|
||||
artifact="${4:-out/build/$preset/$target}"
|
||||
readiness_only=0
|
||||
if [ "${1:-}" = "--readiness-only" ]; then
|
||||
readiness_only=1
|
||||
preset="${2:-linux-clang}"
|
||||
configuration="${3:-Debug}"
|
||||
target="${4:-PanoPainter}"
|
||||
artifact="${5:-out/build/$preset/$target}"
|
||||
fi
|
||||
start="$(date +%s)"
|
||||
root="$(pwd)"
|
||||
|
||||
json_string() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
}
|
||||
|
||||
json_bool() {
|
||||
if [ "$1" = "1" ]; then
|
||||
printf true
|
||||
else
|
||||
printf false
|
||||
fi
|
||||
}
|
||||
|
||||
command_available() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
file_available() {
|
||||
[ -f "$1" ]
|
||||
}
|
||||
|
||||
dir_available() {
|
||||
[ -d "$1" ]
|
||||
}
|
||||
|
||||
package_readiness_json() {
|
||||
windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||
windows_manifest="$root/PanoPainterPackage/Package.appxmanifest"
|
||||
windows_output="$root/PanoPainterPackage/AppPackages"
|
||||
android_standard_gradle="$root/android/android/build.gradle"
|
||||
android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml"
|
||||
android_standard_output="$root/android/android/build/outputs/apk"
|
||||
android_quest_gradle="$root/android/quest/build.gradle"
|
||||
android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml"
|
||||
android_quest_output="$root/android/quest/build/outputs/apk"
|
||||
android_focus_gradle="$root/android/focus/build.gradle"
|
||||
android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml"
|
||||
android_focus_output="$root/android/focus/build/outputs/apk"
|
||||
apple_project="$root/PanoPainter.xcodeproj/project.pbxproj"
|
||||
apple_output="$root/out/package/apple"
|
||||
linux_cmake="$root/linux/CMakeLists.txt"
|
||||
linux_output="$root/out/package/linux/panopainter"
|
||||
webgl_cmake="$root/webgl/CMakeLists.txt"
|
||||
webgl_output="$root/out/package/webgl"
|
||||
|
||||
file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$linux_cmake"; linux_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available cmake; cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$linux_output"; linux_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$webgl_cmake"; webgl_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
printf '['
|
||||
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
|
||||
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
|
||||
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
|
||||
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
|
||||
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
|
||||
printf ',{"kind":"linux-app","status":"blocked","reason":"retained-linux-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter","prerequisites":[{"name":"retained-linux-cmake","available":%s,"detail":%s},{"name":"cmake","available":%s,"detail":"Linux retained app CMake configure/build tool"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"linux-clang"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"linux-app-output","path":%s,"pathType":"Leaf","exists":%s}]}' "$(json_bool "$linux_cmake_exists")" "$(json_string "$linux_cmake")" "$(json_bool "$cmake_exists")" "$(json_string "$linux_output")" "$(json_bool "$linux_output_exists")"
|
||||
printf ',{"kind":"webgl","status":"blocked","reason":"retained-webgl-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter","prerequisites":[{"name":"retained-webgl-cmake","available":%s,"detail":%s},{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$webgl_cmake_exists")" "$(json_string "$webgl_cmake")" "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
|
||||
printf ']'
|
||||
}
|
||||
|
||||
if [ "$readiness_only" -eq 1 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"readiness","exitCode":0,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$elapsed_ms" "$readiness"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
build_exit="$?"
|
||||
if [ "$build_exit" -ne 0 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness"
|
||||
exit "$build_exit"
|
||||
fi
|
||||
|
||||
if [ -e "$artifact" ]; then
|
||||
exit_code=0
|
||||
else
|
||||
exit_code=2
|
||||
fi
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness"
|
||||
exit "$exit_code"
|
||||
200
scripts/automation/platform-build.ps1
Normal file
200
scripts/automation/platform-build.ps1
Normal file
@@ -0,0 +1,200 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64", "android-x64", "android-quest-arm64", "android-focus-arm64"),
|
||||
[string[]]$Targets = @(
|
||||
"pp_foundation",
|
||||
"pp_assets",
|
||||
"pp_paint",
|
||||
"pp_document",
|
||||
"pp_renderer_api",
|
||||
"pp_renderer_gl",
|
||||
"pp_paint_renderer",
|
||||
"pp_ui_core",
|
||||
"pp_platform_api",
|
||||
"pp_app_core",
|
||||
"pano_cli",
|
||||
"pp_foundation_binary_stream_tests",
|
||||
"pp_foundation_event_tests",
|
||||
"pp_foundation_log_tests",
|
||||
"pp_foundation_parse_tests",
|
||||
"pp_foundation_task_queue_tests",
|
||||
"pp_foundation_trace_tests",
|
||||
"pp_assets_brush_package_tests",
|
||||
"pp_assets_image_format_tests",
|
||||
"pp_assets_image_metadata_tests",
|
||||
"pp_assets_image_pixels_tests",
|
||||
"pp_assets_ppi_header_tests",
|
||||
"pp_assets_settings_document_tests",
|
||||
"pp_paint_brush_tests",
|
||||
"pp_paint_blend_tests",
|
||||
"pp_paint_stroke_tests",
|
||||
"pp_paint_stroke_script_tests",
|
||||
"pp_document_tests",
|
||||
"pp_document_ppi_import_tests",
|
||||
"pp_document_ppi_export_tests",
|
||||
"pp_renderer_api_tests",
|
||||
"pp_renderer_gl_capabilities_tests",
|
||||
"pp_renderer_gl_command_plan_tests",
|
||||
"pp_paint_renderer_compositor_tests",
|
||||
"pp_platform_api_tests",
|
||||
"pp_ui_core_color_tests",
|
||||
"pp_ui_core_layout_value_tests",
|
||||
"pp_ui_core_layout_xml_tests",
|
||||
"pp_ui_core_node_lifetime_tests",
|
||||
"pp_ui_core_overlay_lifetime_tests",
|
||||
"pp_app_core_about_menu_tests",
|
||||
"pp_app_core_app_dialog_tests",
|
||||
"pp_app_core_app_preferences_tests",
|
||||
"pp_app_core_app_frame_tests",
|
||||
"pp_app_core_app_thread_tests",
|
||||
"pp_app_core_app_input_tests",
|
||||
"pp_app_core_app_shutdown_tests",
|
||||
"pp_app_core_app_startup_tests",
|
||||
"pp_app_core_app_status_tests",
|
||||
"pp_app_core_command_convert_tests",
|
||||
"pp_app_core_brush_package_export_tests",
|
||||
"pp_app_core_brush_package_import_tests",
|
||||
"pp_app_core_brush_ui_tests",
|
||||
"pp_app_core_canvas_hotkey_tests",
|
||||
"pp_app_core_canvas_tool_ui_tests",
|
||||
"pp_app_core_canvas_view_tests",
|
||||
"pp_app_core_document_animation_tests",
|
||||
"pp_app_core_document_canvas_tests",
|
||||
"pp_app_core_document_cloud_tests",
|
||||
"pp_app_core_document_export_tests",
|
||||
"pp_app_core_document_import_tests",
|
||||
"pp_app_core_document_layer_tests",
|
||||
"pp_app_core_document_platform_io_tests",
|
||||
"pp_app_core_document_recording_tests",
|
||||
"pp_app_core_document_resize_tests",
|
||||
"pp_app_core_document_route_tests",
|
||||
"pp_app_core_document_sharing_tests",
|
||||
"pp_app_core_document_session_tests",
|
||||
"pp_app_core_file_menu_tests",
|
||||
"pp_app_core_grid_ui_tests",
|
||||
"pp_app_core_history_ui_tests",
|
||||
"pp_app_core_main_toolbar_tests",
|
||||
"pp_app_core_quick_ui_tests",
|
||||
"pp_app_core_tools_menu_tests"
|
||||
)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
function Get-VcpkgRoot {
|
||||
$candidates = @()
|
||||
if ($env:VCPKG_ROOT) {
|
||||
$candidates += $env:VCPKG_ROOT
|
||||
}
|
||||
|
||||
$programFiles = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ }
|
||||
$vsYears = @("2026", "2022")
|
||||
$vsEditions = @("Community", "Professional", "Enterprise", "BuildTools", "Preview")
|
||||
foreach ($root in $programFiles) {
|
||||
foreach ($year in $vsYears) {
|
||||
foreach ($edition in $vsEditions) {
|
||||
$candidates += (Join-Path $root "Microsoft Visual Studio\$year\$edition\VC\vcpkg")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath (Join-Path $candidate "vcpkg.exe") -PathType Leaf)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
throw "VCPKG_ROOT was not set and no Visual Studio bundled vcpkg root was found."
|
||||
}
|
||||
|
||||
function Set-VcpkgRootEnvironment {
|
||||
$vcpkgRoot = Get-VcpkgRoot
|
||||
$env:VCPKG_ROOT = $vcpkgRoot
|
||||
return [ordered]@{
|
||||
vcpkgRoot = $vcpkgRoot
|
||||
vcpkgCommand = Join-Path $vcpkgRoot "vcpkg.exe"
|
||||
}
|
||||
}
|
||||
|
||||
$Presets = @(Expand-ArgumentList -Values $Presets)
|
||||
$Targets = @(Expand-ArgumentList -Values $Targets)
|
||||
|
||||
$cmakeCommand = "cmake"
|
||||
$androidToolchain = $null
|
||||
$vcpkgToolchain = $null
|
||||
if ($Presets | Where-Object { $_ -like "*vcpkg*" }) {
|
||||
$vcpkgToolchain = Set-VcpkgRootEnvironment
|
||||
}
|
||||
if ($Presets | Where-Object { $_ -like "android-*" }) {
|
||||
. "$PSScriptRoot\android-sdk-env.ps1"
|
||||
$androidToolchain = Set-AndroidSdkToolchainEnvironment
|
||||
$cmakeCommand = $androidToolchain.cmakeCommand
|
||||
}
|
||||
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($preset in $Presets) {
|
||||
$presetCmakeCommand = $cmakeCommand
|
||||
if ($androidToolchain -and $preset -notlike "android-*") {
|
||||
$presetCmakeCommand = "cmake"
|
||||
}
|
||||
|
||||
& $presetCmakeCommand --preset $preset
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
if ($configureExitCode -ne 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
$results += [ordered]@{
|
||||
preset = $preset
|
||||
stage = "configure"
|
||||
exitCode = $configureExitCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$buildArgs = @("--build", "--preset", $preset)
|
||||
foreach ($target in $Targets) {
|
||||
$buildArgs += @("--target", $target)
|
||||
}
|
||||
|
||||
& $presetCmakeCommand @buildArgs
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
preset = $preset
|
||||
stage = "build"
|
||||
targets = $Targets
|
||||
exitCode = $buildExitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "platform-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidToolchain = $androidToolchain
|
||||
vcpkgToolchain = $vcpkgToolchain
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
exit $overallExitCode
|
||||
65
scripts/automation/platform-build.sh
Normal file
65
scripts/automation/platform-build.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests pp_app_core_about_menu_tests pp_app_core_app_dialog_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}"
|
||||
start="$(date +%s)"
|
||||
|
||||
android_cmake_cmd=""
|
||||
case " $presets " in
|
||||
*" android-"*)
|
||||
# shellcheck disable=SC1091
|
||||
. "$(dirname "$0")/android-sdk-env.sh"
|
||||
set_android_sdk_toolchain_environment || exit 1
|
||||
android_cmake_cmd="$ANDROID_CMAKE_COMMAND"
|
||||
;;
|
||||
esac
|
||||
|
||||
overall_exit=0
|
||||
results=""
|
||||
first_result=1
|
||||
|
||||
build_args=""
|
||||
for target in $targets; do
|
||||
build_args="$build_args --target $target"
|
||||
done
|
||||
|
||||
normalized_presets="$(printf '%s' "$presets" | tr ',' ' ')"
|
||||
for preset in $normalized_presets; do
|
||||
cmake_cmd="cmake"
|
||||
case "$preset" in
|
||||
android-*)
|
||||
cmake_cmd="$android_cmake_cmd"
|
||||
;;
|
||||
esac
|
||||
|
||||
"$cmake_cmd" --preset "$preset"
|
||||
configure_exit="$?"
|
||||
if [ "$configure_exit" -ne 0 ]; then
|
||||
[ "$overall_exit" -eq 0 ] && overall_exit="$configure_exit"
|
||||
result="$(printf '{"preset":"%s","stage":"configure","exitCode":%s}' "$preset" "$configure_exit")"
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
"$cmake_cmd" --build --preset "$preset" $build_args
|
||||
build_exit="$?"
|
||||
[ "$build_exit" -ne 0 ] && [ "$overall_exit" -eq 0 ] && overall_exit="$build_exit"
|
||||
result="$(printf '{"preset":"%s","stage":"build","targets":"%s","exitCode":%s}' "$preset" "$targets" "$build_exit")"
|
||||
fi
|
||||
|
||||
if [ "$first_result" -eq 1 ]; then
|
||||
results="$result"
|
||||
first_result=0
|
||||
else
|
||||
results="$results,$result"
|
||||
fi
|
||||
done
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -n "${ANDROID_CMAKE_COMMAND:-}" ]; then
|
||||
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"androidToolchain":{"sdkRoot":"%s","sdkManagerCommand":"%s","packageUpdates":[{"package":"ndk","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"},{"package":"cmake","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"}],"ndkVersion":"%s","ndkPath":"%s","cmakeVersion":"%s","cmakeCommand":"%s"},"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$ANDROID_SDK_ROOT" "$ANDROID_SDKMANAGER_COMMAND" "$ANDROID_NDK_INSTALLED_BEFORE" "$ANDROID_NDK_AVAILABLE_VERSION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_UPDATE_ACTION" "$ANDROID_CMAKE_INSTALLED_BEFORE" "$ANDROID_CMAKE_AVAILABLE_VERSION" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_UPDATE_ACTION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_HOME" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_COMMAND" "$results"
|
||||
else
|
||||
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$results"
|
||||
fi
|
||||
exit "$overall_exit"
|
||||
304
scripts/automation/quiet-validate.ps1
Normal file
304
scripts/automation/quiet-validate.ps1
Normal file
@@ -0,0 +1,304 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BuildPreset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string[]]$BuildTargets = @("PanoPainter", "pano_cli"),
|
||||
[string]$TestPreset = "desktop-fast",
|
||||
[string]$TestRegex = "",
|
||||
[switch]$Configure,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipTests,
|
||||
[string]$CMakeCommand = "",
|
||||
[string]$CTestCommand = "",
|
||||
[string]$LogDir = "out/logs/quiet-validation",
|
||||
[string]$IgnoreFilterFile = "",
|
||||
[string[]]$IgnorePattern = @(),
|
||||
[int]$FailureTailLines = 0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-CMakeCommand {
|
||||
param([string]$Requested)
|
||||
|
||||
if ($Requested.Length -gt 0) {
|
||||
return $Requested
|
||||
}
|
||||
|
||||
$vsCmake = "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||
if (Test-Path -LiteralPath $vsCmake) {
|
||||
return $vsCmake
|
||||
}
|
||||
|
||||
return "cmake"
|
||||
}
|
||||
|
||||
function Resolve-CTestCommand {
|
||||
param(
|
||||
[string]$Requested,
|
||||
[string]$ResolvedCMake
|
||||
)
|
||||
|
||||
if ($Requested.Length -gt 0) {
|
||||
return $Requested
|
||||
}
|
||||
|
||||
if ($ResolvedCMake.EndsWith("cmake.exe", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$candidate = Join-Path -Path (Split-Path -Parent $ResolvedCMake) -ChildPath "ctest.exe"
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return "ctest"
|
||||
}
|
||||
|
||||
function Read-IgnorePatterns {
|
||||
param(
|
||||
[string]$FilterFile,
|
||||
[string[]]$InlinePatterns
|
||||
)
|
||||
|
||||
$patterns = @()
|
||||
if ($FilterFile.Length -eq 0) {
|
||||
$defaultFile = Join-Path -Path $PSScriptRoot -ChildPath "quiet-validation-ignore.txt"
|
||||
if (Test-Path -LiteralPath $defaultFile) {
|
||||
$FilterFile = $defaultFile
|
||||
}
|
||||
}
|
||||
|
||||
if ($FilterFile.Length -gt 0 -and (Test-Path -LiteralPath $FilterFile)) {
|
||||
$patterns += Get-Content -LiteralPath $FilterFile |
|
||||
Where-Object { $_.Trim().Length -gt 0 -and -not $_.TrimStart().StartsWith("#") }
|
||||
}
|
||||
|
||||
$patterns += $InlinePatterns
|
||||
return @($patterns | Where-Object { $_ -and $_.Length -gt 0 })
|
||||
}
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
if ($null -eq $value) {
|
||||
continue
|
||||
}
|
||||
$expanded += $value -split "[,\s]+" | Where-Object { $_.Length -gt 0 }
|
||||
}
|
||||
return @($expanded)
|
||||
}
|
||||
|
||||
function Limit-LogSlug {
|
||||
param(
|
||||
[string]$Value,
|
||||
[int]$MaxLength = 96
|
||||
)
|
||||
|
||||
if ($Value.Length -le $MaxLength) {
|
||||
return $Value
|
||||
}
|
||||
return $Value.Substring(0, $MaxLength)
|
||||
}
|
||||
|
||||
function Test-IgnoredLine {
|
||||
param(
|
||||
[string]$Line,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
if ($Line -match $pattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Measure-Log {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string[]]$IgnorePatterns
|
||||
)
|
||||
|
||||
$errorPattern = "(?i)(:\s*(fatal\s+)?error\s+[A-Z0-9]+:|^LINK\s*:\s*fatal error|^CMake Error|Errors while running CTest|Unable to find executable|\*\*\*Failed)"
|
||||
$warningPattern = "(?i)(:\s*warning\s+[A-Z0-9]+:|^LINK\s*:\s*warning\s+[A-Z0-9]+:|warning:)"
|
||||
$ctestSummaryPattern = "(\d+)% tests passed, (\d+) tests failed out of (\d+)"
|
||||
|
||||
$lineCount = 0
|
||||
$rawErrors = 0
|
||||
$rawWarnings = 0
|
||||
$visibleErrors = 0
|
||||
$visibleWarnings = 0
|
||||
$ignoredErrors = 0
|
||||
$ignoredWarnings = 0
|
||||
$testsFailed = $null
|
||||
$testsTotal = $null
|
||||
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
++$lineCount
|
||||
$ignored = Test-IgnoredLine -Line $line -Patterns $IgnorePatterns
|
||||
if ($line -match $ctestSummaryPattern) {
|
||||
$testsFailed = [int]$Matches[2]
|
||||
$testsTotal = [int]$Matches[3]
|
||||
}
|
||||
if ($line -match $errorPattern) {
|
||||
++$rawErrors
|
||||
if ($ignored) { ++$ignoredErrors } else { ++$visibleErrors }
|
||||
}
|
||||
if ($line -match $warningPattern) {
|
||||
++$rawWarnings
|
||||
if ($ignored) { ++$ignoredWarnings } else { ++$visibleWarnings }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [ordered]@{
|
||||
lineCount = $lineCount
|
||||
errors = $visibleErrors
|
||||
warnings = $visibleWarnings
|
||||
rawErrors = $rawErrors
|
||||
rawWarnings = $rawWarnings
|
||||
ignoredErrors = $ignoredErrors
|
||||
ignoredWarnings = $ignoredWarnings
|
||||
testsFailed = $testsFailed
|
||||
testsTotal = $testsTotal
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-QuietStep {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Command,
|
||||
[string[]]$Arguments,
|
||||
[string]$LogPath,
|
||||
[string[]]$IgnorePatterns,
|
||||
[int]$FailureTailLines
|
||||
)
|
||||
|
||||
$started = Get-Date
|
||||
$exitCode = 0
|
||||
try {
|
||||
& $Command @Arguments *> $LogPath
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($null -eq $exitCode) {
|
||||
$exitCode = 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
|
||||
$exitCode = 1
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
$summary = Measure-Log -Path $LogPath -IgnorePatterns $IgnorePatterns
|
||||
$result = [ordered]@{
|
||||
name = $Name
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
log = $LogPath
|
||||
summary = $summary
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and (Test-Path -LiteralPath $LogPath)) {
|
||||
$result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines | ForEach-Object { [string]$_ })
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
$resolvedCMake = Resolve-CMakeCommand -Requested $CMakeCommand
|
||||
$resolvedCTest = Resolve-CTestCommand -Requested $CTestCommand -ResolvedCMake $resolvedCMake
|
||||
$BuildTargets = @(Expand-ArgumentList -Values $BuildTargets)
|
||||
$IgnorePattern = @(Expand-ArgumentList -Values $IgnorePattern)
|
||||
$ignorePatterns = Read-IgnorePatterns -FilterFile $IgnoreFilterFile -InlinePatterns $IgnorePattern
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$runId = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
if ($Configure) {
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-configure-$BuildPreset.log"
|
||||
$result = Invoke-QuietStep `
|
||||
-Name "configure:$BuildPreset" `
|
||||
-Command $resolvedCMake `
|
||||
-Arguments @("--preset", $BuildPreset) `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
$targets = @($BuildTargets | Where-Object { $_ -and $_.Length -gt 0 })
|
||||
if ($targets.Count -gt 0) {
|
||||
$safeTargets = Limit-LogSlug -Value (($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-build-$BuildPreset-$Configuration-$safeTargets.log"
|
||||
$buildArgs = @("--build", "--preset", $BuildPreset, "--config", $Configuration, "--target") + $targets
|
||||
$result = Invoke-QuietStep `
|
||||
-Name ("build:{0}:{1}" -f $BuildPreset, $Configuration) `
|
||||
-Command $resolvedCMake `
|
||||
-Arguments $buildArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$result.targets = $targets
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipTests) {
|
||||
$safeRegex = if ($TestRegex.Length -gt 0) {
|
||||
Limit-LogSlug -Value ($TestRegex -replace "[^A-Za-z0-9_.-]", "_")
|
||||
} else {
|
||||
"all"
|
||||
}
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-test-$TestPreset-$Configuration-$safeRegex.log"
|
||||
$testArgs = @("--preset", $TestPreset, "--build-config", $Configuration, "--output-on-failure")
|
||||
if ($TestRegex.Length -gt 0) {
|
||||
$testArgs += @("-R", $TestRegex)
|
||||
}
|
||||
$result = Invoke-QuietStep `
|
||||
-Name ("test:{0}:{1}" -f $TestPreset, $Configuration) `
|
||||
-Command $resolvedCTest `
|
||||
-Arguments $testArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
if ($TestRegex.Length -gt 0) {
|
||||
$result.testRegex = $TestRegex
|
||||
}
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
$summaryPath = Join-Path -Path $LogDir -ChildPath "$runId-summary.json"
|
||||
$payload = [ordered]@{
|
||||
command = "quiet-validate"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
buildPreset = $BuildPreset
|
||||
configuration = $Configuration
|
||||
testPreset = $TestPreset
|
||||
logDir = $LogDir
|
||||
summary = $summaryPath
|
||||
ignoreFilterFile = $IgnoreFilterFile
|
||||
ignorePatternCount = $ignorePatterns.Count
|
||||
results = $results
|
||||
}
|
||||
|
||||
$payload | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $summaryPath -Encoding utf8
|
||||
$payload | ConvertTo-Json -Compress -Depth 8
|
||||
exit $overallExitCode
|
||||
13
scripts/automation/quiet-validation-ignore.txt
Normal file
13
scripts/automation/quiet-validation-ignore.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Regex patterns for warnings/noise hidden from quiet validation summaries.
|
||||
# The full logs still contain these lines; this file only affects visible counts.
|
||||
The vcpkg manifest was disabled
|
||||
warning C4201:
|
||||
warning C4267:
|
||||
warning C5311:
|
||||
warning C4018:
|
||||
warning C4244:
|
||||
warning C4189:
|
||||
warning C4305:
|
||||
warning C4099:
|
||||
warning LNK4099: PDB 'yuv\.pdb'
|
||||
warning LNK4098: defaultlib 'MSVCRT' conflicts
|
||||
22
scripts/automation/test.ps1
Normal file
22
scripts/automation/test.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Preset = "desktop-fast",
|
||||
[string]$Configuration = "Debug"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
|
||||
& ctest --preset $Preset --build-config $Configuration
|
||||
$exitCode = $LASTEXITCODE
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "test"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
exit $exitCode
|
||||
12
scripts/automation/test.sh
Normal file
12
scripts/automation/test.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-desktop-fast}"
|
||||
configuration="${2:-Debug}"
|
||||
start="$(date +%s)"
|
||||
ctest --preset "$preset" --build-config "$configuration"
|
||||
exit_code="$?"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"test","preset":"%s","configuration":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
144
scripts/dev/check_package_smoke_readiness.py
Normal file
144
scripts/dev/check_package_smoke_readiness.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify package-smoke wrappers report the expected package readiness matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXPECTED_PACKAGE_KINDS = [
|
||||
"windows-appx",
|
||||
"android-standard-apk",
|
||||
"android-quest-apk",
|
||||
"android-focus-apk",
|
||||
"apple-bundle",
|
||||
"linux-app",
|
||||
"webgl",
|
||||
]
|
||||
|
||||
EXPECTED_CMAKE_PACKAGE_TARGETS = [
|
||||
"panopainter_package_readiness",
|
||||
"panopainter_windows_app_package_smoke",
|
||||
"panopainter_android_standard_native_package",
|
||||
"panopainter_android_vr_native_package_configure",
|
||||
"panopainter_android_native_package_smoke",
|
||||
"panopainter_linux_webgl_package_readiness",
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def powershell_package_kinds(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8")
|
||||
match = re.search(r"\[string\[\]\]\$PackageKinds\s*=\s*@\((.*?)\n\s*\)", script, re.S)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find PackageKinds default in package-smoke.ps1")
|
||||
return sorted(re.findall(r'"([^"]+)"', match.group(1)))
|
||||
|
||||
|
||||
def shell_package_kinds(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8")
|
||||
return sorted(set(re.findall(r'"kind":"([^"]+)"', script)))
|
||||
|
||||
|
||||
def count_regex(root: Path, patterns: dict[str, str]) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for script_name, pattern in patterns.items():
|
||||
text = (root / "scripts" / "automation" / script_name).read_text(encoding="utf-8")
|
||||
counts[script_name] = len(re.findall(pattern, text))
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
expected = sorted(EXPECTED_PACKAGE_KINDS)
|
||||
ps_kinds = powershell_package_kinds(root)
|
||||
sh_kinds = shell_package_kinds(root)
|
||||
debt_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r'debt\s*=\s*"DEBT-0011"',
|
||||
"package-smoke.sh": r'"debt":"DEBT-0011"',
|
||||
})
|
||||
blocked_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r'-Status\s+"blocked"',
|
||||
"package-smoke.sh": r'"status":"blocked"',
|
||||
})
|
||||
readiness_mode_counts = {
|
||||
"package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8").count("ReadinessOnly"),
|
||||
"package-smoke.sh": (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8").count("readiness_only"),
|
||||
}
|
||||
retained_android_native_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r"retained-native-cmake-check",
|
||||
"package-smoke.sh": r"retained-native-cmake-check",
|
||||
})
|
||||
retained_platform_cmake_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r"retained-(linux|webgl)-cmake",
|
||||
"package-smoke.sh": r"retained-(linux|webgl)-cmake",
|
||||
})
|
||||
cmake_package_module = (root / "cmake" / "PanoPainterPackageTargets.cmake").read_text(encoding="utf-8")
|
||||
root_cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
|
||||
missing = {
|
||||
"package-smoke.ps1": [kind for kind in expected if kind not in ps_kinds],
|
||||
"package-smoke.sh": [kind for kind in expected if kind not in sh_kinds],
|
||||
}
|
||||
unexpected = {
|
||||
"package-smoke.ps1": [kind for kind in ps_kinds if kind not in expected],
|
||||
"package-smoke.sh": [kind for kind in sh_kinds if kind not in expected],
|
||||
}
|
||||
debt_thresholds = {
|
||||
"package-smoke.ps1": 1,
|
||||
"package-smoke.sh": len(expected),
|
||||
}
|
||||
debt_complete = {name: count >= debt_thresholds[name] for name, count in debt_counts.items()}
|
||||
blocked_complete = {name: count >= len(expected) for name, count in blocked_counts.items()}
|
||||
readiness_mode_present = {name: count > 0 for name, count in readiness_mode_counts.items()}
|
||||
retained_android_native_complete = {
|
||||
name: count >= 3 for name, count in retained_android_native_counts.items()
|
||||
}
|
||||
retained_platform_cmake_complete = {
|
||||
name: count >= 2 for name, count in retained_platform_cmake_counts.items()
|
||||
}
|
||||
cmake_package_targets_present = {
|
||||
target: target in cmake_package_module for target in EXPECTED_CMAKE_PACKAGE_TARGETS
|
||||
}
|
||||
cmake_package_module_included = "include(PanoPainterPackageTargets)" in root_cmake
|
||||
|
||||
ok = (
|
||||
all(not values for values in missing.values())
|
||||
and all(not values for values in unexpected.values())
|
||||
and all(debt_complete.values())
|
||||
and all(blocked_complete.values())
|
||||
and all(readiness_mode_present.values())
|
||||
and all(retained_android_native_complete.values())
|
||||
and all(retained_platform_cmake_complete.values())
|
||||
and all(cmake_package_targets_present.values())
|
||||
and cmake_package_module_included
|
||||
)
|
||||
|
||||
print(json.dumps({
|
||||
"ok": ok,
|
||||
"expectedPackageKinds": expected,
|
||||
"packageKinds": {
|
||||
"package-smoke.ps1": ps_kinds,
|
||||
"package-smoke.sh": sh_kinds,
|
||||
},
|
||||
"missing": missing,
|
||||
"unexpected": unexpected,
|
||||
"debtComplete": debt_complete,
|
||||
"blockedComplete": blocked_complete,
|
||||
"readinessModePresent": readiness_mode_present,
|
||||
"retainedAndroidNativeComplete": retained_android_native_complete,
|
||||
"retainedPlatformCmakeComplete": retained_platform_cmake_complete,
|
||||
"cmakePackageTargetsPresent": cmake_package_targets_present,
|
||||
"cmakePackageModuleIncluded": cmake_package_module_included,
|
||||
}, separators=(",", ":")))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
153
scripts/dev/check_platform_build_targets.py
Normal file
153
scripts/dev/check_platform_build_targets.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify platform-build wrappers include the current headless target matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REQUIRED_COMPONENT_TARGETS = [
|
||||
"pp_foundation",
|
||||
"pp_assets",
|
||||
"pp_paint",
|
||||
"pp_document",
|
||||
"pp_renderer_api",
|
||||
"pp_renderer_gl",
|
||||
"pp_paint_renderer",
|
||||
"pp_ui_core",
|
||||
"pp_platform_api",
|
||||
"pp_app_core",
|
||||
"pano_cli",
|
||||
]
|
||||
|
||||
REQUIRED_ANDROID_PRESETS = [
|
||||
"android-arm64",
|
||||
"android-x64",
|
||||
"android-quest-arm64",
|
||||
"android-focus-arm64",
|
||||
]
|
||||
|
||||
EXPECTED_CMAKE_PLATFORM_TARGETS = [
|
||||
"panopainter_platform_build_headless",
|
||||
"panopainter_platform_build_android_assets",
|
||||
"panopainter_platform_build_vcpkg_ui_core",
|
||||
"panopainter_platform_build_apple_remote",
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def cmake_test_targets(root: Path) -> list[str]:
|
||||
cmake_lists = root / "tests" / "CMakeLists.txt"
|
||||
text = cmake_lists.read_text(encoding="utf-8")
|
||||
return sorted(set(re.findall(r"^\s*add_executable\(([^\s\)]+)", text, re.MULTILINE)))
|
||||
|
||||
|
||||
def powershell_default_targets(root: Path) -> list[str]:
|
||||
script = root / "scripts" / "automation" / "platform-build.ps1"
|
||||
targets: list[str] = []
|
||||
in_targets = False
|
||||
|
||||
for line in script.read_text(encoding="utf-8").splitlines():
|
||||
if "[string[]]$Targets" in line:
|
||||
in_targets = True
|
||||
if not in_targets:
|
||||
continue
|
||||
|
||||
targets.extend(re.findall(r'"([^"]+)"', line))
|
||||
if in_targets and line.strip() == ")":
|
||||
break
|
||||
|
||||
return sorted(set(targets))
|
||||
|
||||
|
||||
def powershell_default_presets(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "platform-build.ps1").read_text(encoding="utf-8")
|
||||
match = re.search(r"\[string\[\]\]\$Presets\s*=\s*@\((.*?)\)", script, re.S)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default presets in platform-build.ps1")
|
||||
return sorted(set(re.findall(r'"([^"]+)"', match.group(1))))
|
||||
|
||||
|
||||
def shell_default_targets(root: Path) -> list[str]:
|
||||
script = root / "scripts" / "automation" / "platform-build.sh"
|
||||
text = script.read_text(encoding="utf-8")
|
||||
match = re.search(r'targets="\$\{[^:]+:-(.*)\}"', text)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default targets in platform-build.sh")
|
||||
return sorted(set(match.group(1).split()))
|
||||
|
||||
|
||||
def shell_default_presets(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "platform-build.sh").read_text(encoding="utf-8")
|
||||
match = re.search(r'presets="\$\{[^:]+:-(.*)\}"', script)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default presets in platform-build.sh")
|
||||
return sorted(set(match.group(1).split()))
|
||||
|
||||
|
||||
def missing(expected: list[str], actual: list[str]) -> list[str]:
|
||||
actual_set = set(actual)
|
||||
return [target for target in expected if target not in actual_set]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
expected = sorted(set(REQUIRED_COMPONENT_TARGETS + cmake_test_targets(root)))
|
||||
ps_targets = powershell_default_targets(root)
|
||||
sh_targets = shell_default_targets(root)
|
||||
ps_presets = powershell_default_presets(root)
|
||||
sh_presets = shell_default_presets(root)
|
||||
cmake_platform_module = (root / "cmake" / "PanoPainterPlatformTargets.cmake").read_text(encoding="utf-8")
|
||||
root_cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
cmake_platform_targets_present = {
|
||||
target: target in cmake_platform_module for target in EXPECTED_CMAKE_PLATFORM_TARGETS
|
||||
}
|
||||
cmake_platform_module_included = "include(PanoPainterPlatformTargets)" in root_cmake
|
||||
android_sdk_env = {
|
||||
"android-sdk-env.ps1": (root / "scripts" / "automation" / "android-sdk-env.ps1").read_text(encoding="utf-8"),
|
||||
"android-sdk-env.sh": (root / "scripts" / "automation" / "android-sdk-env.sh").read_text(encoding="utf-8"),
|
||||
}
|
||||
android_sdkmanager_update_support = {
|
||||
name: all(token in text for token in ("sdkmanager", "--list", "--install", "ndk", "cmake"))
|
||||
for name, text in android_sdk_env.items()
|
||||
}
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"expectedTargetCount": len(expected),
|
||||
"powershellTargetCount": len(ps_targets),
|
||||
"shellTargetCount": len(sh_targets),
|
||||
"expectedAndroidPresets": REQUIRED_ANDROID_PRESETS,
|
||||
"defaultPresets": {
|
||||
"platform-build.ps1": ps_presets,
|
||||
"platform-build.sh": sh_presets,
|
||||
},
|
||||
"cmakePlatformTargetsPresent": cmake_platform_targets_present,
|
||||
"cmakePlatformModuleIncluded": cmake_platform_module_included,
|
||||
"androidSdkmanagerUpdateSupport": android_sdkmanager_update_support,
|
||||
"missing": {
|
||||
"platform-build.ps1.targets": missing(expected, ps_targets),
|
||||
"platform-build.sh.targets": missing(expected, sh_targets),
|
||||
"platform-build.ps1.presets": missing(REQUIRED_ANDROID_PRESETS, ps_presets),
|
||||
"platform-build.sh.presets": missing(REQUIRED_ANDROID_PRESETS, sh_presets),
|
||||
},
|
||||
}
|
||||
result["ok"] = (
|
||||
all(not values for values in result["missing"].values())
|
||||
and all(cmake_platform_targets_present.values())
|
||||
and cmake_platform_module_included
|
||||
and all(android_sdkmanager_update_support.values())
|
||||
)
|
||||
|
||||
print(json.dumps(result, separators=(",", ":")))
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
61
scripts/dev/check_retained_platform_cmake.py
Normal file
61
scripts/dev/check_retained_platform_cmake.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard retained non-root platform CMake files during Phase 6 migration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RETAINED_PLATFORM_CMAKE = [
|
||||
Path("linux/CMakeLists.txt"),
|
||||
Path("webgl/CMakeLists.txt"),
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def cmake_minimum_version(text: str) -> tuple[int, ...] | None:
|
||||
match = re.search(r"cmake_minimum_required\s*\(\s*VERSION\s+([0-9.]+)", text, re.I)
|
||||
if not match:
|
||||
return None
|
||||
return tuple(int(part) for part in match.group(1).split("."))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
results: dict[str, object] = {}
|
||||
ok = True
|
||||
|
||||
for path in RETAINED_PLATFORM_CMAKE:
|
||||
text = (root / path).read_text(encoding="utf-8")
|
||||
minimum_version = cmake_minimum_version(text)
|
||||
has_target_cxx23 = "target_compile_features(panopainter PRIVATE cxx_std_23)" in text
|
||||
has_cxx_extensions_off = "CXX_EXTENSIONS OFF" in text
|
||||
uses_global_cxx_standard_flag = bool(re.search(r"-std=c\+\+14|-std=c\+\+17|-std=c\+\+20", text))
|
||||
platform_ok = (
|
||||
minimum_version is not None
|
||||
and minimum_version >= (3, 10)
|
||||
and has_target_cxx23
|
||||
and has_cxx_extensions_off
|
||||
and not uses_global_cxx_standard_flag
|
||||
)
|
||||
ok = ok and platform_ok
|
||||
results[str(path)] = {
|
||||
"ok": platform_ok,
|
||||
"minimumVersion": ".".join(str(part) for part in minimum_version) if minimum_version else None,
|
||||
"hasTargetCxx23": has_target_cxx23,
|
||||
"hasCxxExtensionsOff": has_cxx_extensions_off,
|
||||
"usesGlobalCxxStandardFlag": uses_global_cxx_standard_flag,
|
||||
}
|
||||
|
||||
print(json.dumps({"ok": ok, "platforms": results}, separators=(",", ":")))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
608
scripts/dev/clangd_nav.py
Normal file
608
scripts/dev/clangd_nav.py
Normal file
@@ -0,0 +1,608 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small clangd navigation helper for agent-friendly C++ code lookup.
|
||||
|
||||
Examples:
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 410 --column 30
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 192 --column 43 --path-regex "src[\\\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Pattern
|
||||
|
||||
|
||||
DEFAULT_BUILD_DIRS = (
|
||||
"out/build/windows-clangcl-asan",
|
||||
"out/build/android-arm64",
|
||||
)
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _find_compile_commands_dir(repo_root: Path, requested: str | None) -> Path:
|
||||
if requested:
|
||||
path = Path(requested).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = repo_root / path
|
||||
if path.is_file():
|
||||
path = path.parent
|
||||
if not (path / "compile_commands.json").exists():
|
||||
raise SystemExit(f"compile_commands.json not found in {path}")
|
||||
return path.resolve()
|
||||
|
||||
env_dir = os.environ.get("PP_CLANGD_COMPILE_COMMANDS_DIR")
|
||||
if env_dir:
|
||||
return _find_compile_commands_dir(repo_root, env_dir)
|
||||
|
||||
for candidate in DEFAULT_BUILD_DIRS:
|
||||
path = repo_root / candidate
|
||||
if (path / "compile_commands.json").exists():
|
||||
return path.resolve()
|
||||
|
||||
matches = sorted((repo_root / "out" / "build").glob("*/compile_commands.json"))
|
||||
if matches:
|
||||
return matches[0].parent.resolve()
|
||||
|
||||
raise SystemExit(
|
||||
"No compile_commands.json found. Configure a Ninja CMake preset first, "
|
||||
"or pass --compile-commands-dir."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_file(repo_root: Path, file_arg: str) -> Path:
|
||||
path = Path(file_arg).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = repo_root / path
|
||||
if not path.exists():
|
||||
raise SystemExit(f"file not found: {path}")
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _read_lsp_message(stream: Any) -> dict[str, Any] | None:
|
||||
content_length: int | None = None
|
||||
while True:
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
return None
|
||||
if line in (b"\r\n", b"\n"):
|
||||
break
|
||||
name, _, value = line.decode("ascii", errors="replace").partition(":")
|
||||
if name.lower() == "content-length":
|
||||
content_length = int(value.strip())
|
||||
|
||||
if content_length is None:
|
||||
return None
|
||||
|
||||
payload = stream.read(content_length)
|
||||
if not payload:
|
||||
return None
|
||||
return json.loads(payload.decode("utf-8"))
|
||||
|
||||
|
||||
def _write_lsp_message(stream: Any, message: dict[str, Any]) -> None:
|
||||
payload = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
||||
header = f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii")
|
||||
stream.write(header + payload)
|
||||
stream.flush()
|
||||
|
||||
|
||||
class ClangdClient:
|
||||
def __init__(
|
||||
self,
|
||||
clangd: str,
|
||||
compile_commands_dir: Path,
|
||||
timeout_seconds: float,
|
||||
background_index: bool) -> None:
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._next_id = 1
|
||||
self._responses: dict[int, dict[str, Any]] = {}
|
||||
self._condition = threading.Condition()
|
||||
self._messages: "queue.Queue[dict[str, Any]]" = queue.Queue()
|
||||
clangd_args = [
|
||||
clangd,
|
||||
f"--compile-commands-dir={compile_commands_dir}",
|
||||
"--log=error",
|
||||
]
|
||||
if not background_index:
|
||||
clangd_args.append("--background-index=false")
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
clangd_args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if self._process.stdin is None or self._process.stdout is None:
|
||||
raise RuntimeError("failed to open clangd stdio pipes")
|
||||
|
||||
self._stdin = self._process.stdin
|
||||
self._stdout = self._process.stdout
|
||||
self._reader = threading.Thread(target=self._reader_loop, daemon=True)
|
||||
self._reader.start()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self.notify("exit", {})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=2)
|
||||
except Exception:
|
||||
self._process.kill()
|
||||
|
||||
def _reader_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
message = _read_lsp_message(self._stdout)
|
||||
except Exception as exc:
|
||||
message = {"error": {"message": f"failed to read clangd response: {exc}"}}
|
||||
if message is None:
|
||||
return
|
||||
if "id" in message:
|
||||
with self._condition:
|
||||
self._responses[int(message["id"])] = message
|
||||
self._condition.notify_all()
|
||||
else:
|
||||
self._messages.put(message)
|
||||
|
||||
def request(self, method: str, params: dict[str, Any]) -> Any:
|
||||
request_id = self._next_id
|
||||
self._next_id += 1
|
||||
_write_lsp_message(
|
||||
self._stdin,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
},
|
||||
)
|
||||
deadline = time.monotonic() + self._timeout_seconds
|
||||
with self._condition:
|
||||
while request_id not in self._responses:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(f"clangd request timed out: {method}")
|
||||
self._condition.wait(remaining)
|
||||
response = self._responses.pop(request_id)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(response["error"].get("message", "clangd request failed"))
|
||||
return response.get("result")
|
||||
|
||||
def notify(self, method: str, params: dict[str, Any]) -> None:
|
||||
_write_lsp_message(
|
||||
self._stdin,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _position_params(file_path: Path, line: int, column: int) -> dict[str, Any]:
|
||||
if line < 1 or column < 1:
|
||||
raise SystemExit("--line and --column are 1-based and must be positive")
|
||||
return {
|
||||
"textDocument": { "uri": file_path.as_uri() },
|
||||
"position": { "line": line - 1, "character": column - 1 },
|
||||
}
|
||||
|
||||
|
||||
def _range_to_json(range_value: dict[str, Any]) -> dict[str, Any]:
|
||||
start = range_value["start"]
|
||||
end = range_value["end"]
|
||||
return {
|
||||
"start": { "line": start["line"] + 1, "column": start["character"] + 1 },
|
||||
"end": { "line": end["line"] + 1, "column": end["character"] + 1 },
|
||||
}
|
||||
|
||||
|
||||
def _location_to_json(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if "targetUri" in value:
|
||||
uri = value["targetUri"]
|
||||
range_value = value.get("targetRange", value.get("targetSelectionRange"))
|
||||
else:
|
||||
uri = value["uri"]
|
||||
range_value = value["range"]
|
||||
return {
|
||||
"uri": uri,
|
||||
"path": _uri_to_path(uri),
|
||||
"range": _range_to_json(range_value),
|
||||
}
|
||||
|
||||
|
||||
def _uri_to_path(uri: str) -> str:
|
||||
if uri.startswith("file:///"):
|
||||
raw = uri[8:]
|
||||
if len(raw) >= 3 and raw[1] == ":":
|
||||
return raw.replace("/", "\\")
|
||||
return "/" + raw
|
||||
return uri
|
||||
|
||||
|
||||
def _locations_to_json(result: Any) -> list[dict[str, Any]]:
|
||||
if result is None:
|
||||
return []
|
||||
if isinstance(result, dict):
|
||||
return [_location_to_json(result)]
|
||||
return [_location_to_json(item) for item in result]
|
||||
|
||||
|
||||
def _symbols_to_json(symbols: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
def convert(symbol: dict[str, Any]) -> dict[str, Any]:
|
||||
item = {
|
||||
"name": symbol.get("name", ""),
|
||||
"detail": symbol.get("detail", ""),
|
||||
"kind": symbol.get("kind", 0),
|
||||
"range": _range_to_json(symbol["range"]),
|
||||
"selectionRange": _range_to_json(symbol.get("selectionRange", symbol["range"])),
|
||||
}
|
||||
children = symbol.get("children")
|
||||
if children:
|
||||
item["children"] = [convert(child) for child in children]
|
||||
return item
|
||||
|
||||
return [convert(symbol) for symbol in symbols or []]
|
||||
|
||||
|
||||
def _flatten_symbols(symbols: list[dict[str, Any]], parent: str = "") -> list[dict[str, Any]]:
|
||||
flattened: list[dict[str, Any]] = []
|
||||
for symbol in symbols:
|
||||
qualified_name = f"{parent}::{symbol['name']}" if parent else symbol["name"]
|
||||
item = {
|
||||
"name": symbol["name"],
|
||||
"qualifiedName": qualified_name,
|
||||
"detail": symbol.get("detail", ""),
|
||||
"kind": symbol.get("kind", 0),
|
||||
"range": symbol["range"],
|
||||
"selectionRange": symbol["selectionRange"],
|
||||
}
|
||||
flattened.append(item)
|
||||
flattened.extend(_flatten_symbols(symbol.get("children", []), qualified_name))
|
||||
return flattened
|
||||
|
||||
|
||||
def _limit_results(values: list[dict[str, Any]], max_results: int) -> tuple[list[dict[str, Any]], bool]:
|
||||
if max_results < 1:
|
||||
return values, False
|
||||
return values[:max_results], len(values) > max_results
|
||||
|
||||
|
||||
def _hover_to_json(result: Any) -> dict[str, Any] | None:
|
||||
if not result:
|
||||
return None
|
||||
contents = result.get("contents")
|
||||
if isinstance(contents, dict):
|
||||
value = contents.get("value", "")
|
||||
elif isinstance(contents, list):
|
||||
value = "\n".join(str(item.get("value", item)) if isinstance(item, dict) else str(item) for item in contents)
|
||||
else:
|
||||
value = str(contents)
|
||||
output = { "contents": value }
|
||||
if "range" in result:
|
||||
output["range"] = _range_to_json(result["range"])
|
||||
return output
|
||||
|
||||
|
||||
def _compile_optional_regex(pattern: str | None, option_name: str, ignore_case: bool) -> Pattern[str] | None:
|
||||
if not pattern:
|
||||
return None
|
||||
flags = re.IGNORECASE if ignore_case else 0
|
||||
try:
|
||||
return re.compile(pattern, flags)
|
||||
except re.error as exc:
|
||||
raise SystemExit(f"invalid {option_name}: {exc}") from exc
|
||||
|
||||
|
||||
def _regex_matches(regex: Pattern[str] | None, value: str) -> bool:
|
||||
return regex is None or regex.search(value) is not None
|
||||
|
||||
|
||||
def _filter_flat_symbols(
|
||||
symbols: list[dict[str, Any]],
|
||||
name_substring: str | None,
|
||||
name_regex: Pattern[str] | None,
|
||||
detail_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
filtered = symbols
|
||||
if name_substring:
|
||||
needle = name_substring.lower()
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if needle in symbol["qualifiedName"].lower()
|
||||
]
|
||||
if name_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if name_regex.search(symbol["qualifiedName"])
|
||||
]
|
||||
if detail_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if detail_regex.search(symbol.get("detail", ""))
|
||||
]
|
||||
return filtered
|
||||
|
||||
|
||||
def _filter_locations(
|
||||
locations: list[dict[str, Any]],
|
||||
path_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not path_regex:
|
||||
return locations
|
||||
return [
|
||||
location for location in locations
|
||||
if _regex_matches(path_regex, location.get("path", ""))
|
||||
or _regex_matches(path_regex, location.get("uri", ""))
|
||||
]
|
||||
|
||||
|
||||
def _run_self_test() -> int:
|
||||
name_regex = _compile_optional_regex(r"node(panel|dialog)::open_.*", "--name-regex", True)
|
||||
detail_regex = _compile_optional_regex(r"export.*plan", "--detail-regex", True)
|
||||
path_regex = _compile_optional_regex(r"src[\\/]app(_dialogs)?\.cpp$", "--path-regex", True)
|
||||
case_sensitive_regex = _compile_optional_regex(r"Brush", "--name-regex", False)
|
||||
|
||||
symbols = [
|
||||
{
|
||||
"qualifiedName": "NodePanel::open_project",
|
||||
"detail": "void()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "NodeDialog::open_export",
|
||||
"detail": "ExportTargetPlan()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "Brush::open_project",
|
||||
"detail": "BrushPlan()",
|
||||
},
|
||||
]
|
||||
name_matches = _filter_flat_symbols(symbols, None, name_regex, None)
|
||||
detail_matches = _filter_flat_symbols(symbols, None, None, detail_regex)
|
||||
case_sensitive_matches = _filter_flat_symbols(symbols, None, case_sensitive_regex, None)
|
||||
|
||||
locations = [
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app_dialogs.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app_dialogs.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\docs\modernization\roadmap.md",
|
||||
"uri": "file:///D:/Dev/panopainter/docs/modernization/roadmap.md",
|
||||
},
|
||||
]
|
||||
path_matches = _filter_locations(locations, path_regex)
|
||||
|
||||
checks = {
|
||||
"nameRegexMatchesAlternationAndWildcard": len(name_matches) == 2,
|
||||
"detailRegexMatchesSymbolDetail": len(detail_matches) == 1
|
||||
and detail_matches[0]["qualifiedName"] == "NodeDialog::open_export",
|
||||
"caseSensitiveRegexHonorsNoIgnoreCase": len(case_sensitive_matches) == 1
|
||||
and case_sensitive_matches[0]["qualifiedName"] == "Brush::open_project",
|
||||
"pathRegexFiltersLocations": len(path_matches) == 2
|
||||
and all("src" in location["path"] for location in path_matches),
|
||||
}
|
||||
ok = all(checks.values())
|
||||
print(json.dumps(
|
||||
{
|
||||
"ok": ok,
|
||||
"command": "self-test",
|
||||
"checks": checks,
|
||||
},
|
||||
indent=2,
|
||||
))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def _open_document(client: ClangdClient, file_path: Path) -> None:
|
||||
language_id = "cpp"
|
||||
if file_path.suffix.lower() in { ".h", ".hpp", ".hh", ".hxx" }:
|
||||
language_id = "cpp"
|
||||
elif file_path.suffix.lower() == ".c":
|
||||
language_id = "c"
|
||||
|
||||
client.notify(
|
||||
"textDocument/didOpen",
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": file_path.as_uri(),
|
||||
"languageId": language_id,
|
||||
"version": 1,
|
||||
"text": file_path.read_text(encoding="utf-8", errors="replace"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
if args.command == "self-test":
|
||||
return _run_self_test()
|
||||
|
||||
if not args.file:
|
||||
raise SystemExit("--file is required for clangd navigation commands")
|
||||
|
||||
symbol_filters = [args.name, args.name_regex, args.detail_regex]
|
||||
if any(symbol_filters) and args.command != "symbols":
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex are only supported by the symbols command")
|
||||
if args.path_regex and args.command not in { "definition", "declaration", "implementation", "references" }:
|
||||
raise SystemExit("--path-regex is only supported by location commands")
|
||||
if args.hierarchical and any(symbol_filters):
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex require flat symbols; omit --hierarchical")
|
||||
|
||||
repo_root = _repo_root()
|
||||
compile_commands_dir = _find_compile_commands_dir(repo_root, args.compile_commands_dir)
|
||||
file_path = _resolve_file(repo_root, args.file)
|
||||
|
||||
name_regex = _compile_optional_regex(args.name_regex, "--name-regex", args.ignore_case)
|
||||
detail_regex = _compile_optional_regex(args.detail_regex, "--detail-regex", args.ignore_case)
|
||||
path_regex = _compile_optional_regex(args.path_regex, "--path-regex", args.ignore_case)
|
||||
|
||||
if args.command == "references" and not args.background_index and not args.allow_incomplete_references:
|
||||
raise SystemExit(
|
||||
"references may be incomplete without clangd background indexing. "
|
||||
"Pass --background-index for a broader best-effort query or "
|
||||
"--allow-incomplete-references for current-translation-unit lookup."
|
||||
)
|
||||
|
||||
client = ClangdClient(args.clangd, compile_commands_dir, args.timeout, args.background_index)
|
||||
try:
|
||||
client.request(
|
||||
"initialize",
|
||||
{
|
||||
"processId": None,
|
||||
"rootUri": repo_root.as_uri(),
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"definition": { "linkSupport": True },
|
||||
"declaration": { "linkSupport": True },
|
||||
"implementation": { "linkSupport": True },
|
||||
"references": {},
|
||||
"hover": {},
|
||||
"documentSymbol": { "hierarchicalDocumentSymbolSupport": True },
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
client.notify("initialized", {})
|
||||
_open_document(client, file_path)
|
||||
|
||||
command = args.command
|
||||
result: Any
|
||||
result_count: int | None = None
|
||||
truncated = False
|
||||
if command == "symbols":
|
||||
symbols = _symbols_to_json(
|
||||
client.request("textDocument/documentSymbol", { "textDocument": { "uri": file_path.as_uri() } })
|
||||
)
|
||||
if args.hierarchical:
|
||||
result = symbols
|
||||
result_count = len(symbols)
|
||||
else:
|
||||
flattened = _flatten_symbols(symbols)
|
||||
flattened = _filter_flat_symbols(flattened, args.name, name_regex, detail_regex)
|
||||
result_count = len(flattened)
|
||||
result, truncated = _limit_results(flattened, args.max_results)
|
||||
elif command == "hover":
|
||||
result = _hover_to_json(client.request("textDocument/hover", _position_params(file_path, args.line, args.column)))
|
||||
elif command == "references":
|
||||
params = _position_params(file_path, args.line, args.column)
|
||||
params["context"] = { "includeDeclaration": args.include_declaration }
|
||||
locations = _locations_to_json(client.request("textDocument/references", params))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
else:
|
||||
method = {
|
||||
"definition": "textDocument/definition",
|
||||
"declaration": "textDocument/declaration",
|
||||
"implementation": "textDocument/implementation",
|
||||
}[command]
|
||||
locations = _locations_to_json(client.request(method, _position_params(file_path, args.line, args.column)))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
|
||||
print(json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"command": command,
|
||||
"file": str(file_path),
|
||||
"compileCommandsDir": str(compile_commands_dir),
|
||||
"backgroundIndex": args.background_index,
|
||||
"referenceCompleteness": (
|
||||
"not-applicable" if command != "references"
|
||||
else ("best-effort-background-index" if args.background_index else "current-translation-unit-only")
|
||||
),
|
||||
"filters": {
|
||||
"name": args.name,
|
||||
"nameRegex": args.name_regex,
|
||||
"detailRegex": args.detail_regex,
|
||||
"pathRegex": args.path_regex,
|
||||
"ignoreCase": args.ignore_case,
|
||||
},
|
||||
"resultCount": result_count,
|
||||
"truncated": truncated,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
))
|
||||
return 0
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="Navigate C++ symbols through clangd JSON-RPC.")
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=("definition", "declaration", "implementation", "references", "hover", "symbols", "self-test"),
|
||||
)
|
||||
parser.add_argument("--file", help="Source/header file to open.")
|
||||
parser.add_argument("--line", type=int, default=1, help="1-based line for position commands.")
|
||||
parser.add_argument("--column", type=int, default=1, help="1-based column for position commands.")
|
||||
parser.add_argument(
|
||||
"--compile-commands-dir",
|
||||
help="Directory containing compile_commands.json. Defaults to PP_CLANGD_COMPILE_COMMANDS_DIR or known build dirs.",
|
||||
)
|
||||
parser.add_argument("--clangd", default="clangd", help="clangd executable path.")
|
||||
parser.add_argument("--timeout", type=float, default=20.0, help="Request timeout in seconds.")
|
||||
parser.add_argument("--name", help="Case-insensitive symbol-name filter for symbols command.")
|
||||
parser.add_argument("--name-regex", help="Regex filter for symbols command, matched against qualifiedName.")
|
||||
parser.add_argument("--detail-regex", help="Regex filter for symbols command, matched against detail.")
|
||||
parser.add_argument("--path-regex", help="Regex filter for definition/declaration/implementation/references paths.")
|
||||
parser.add_argument(
|
||||
"--ignore-case",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Use case-insensitive regex matching for --name-regex, --detail-regex, and --path-regex. Enabled by default.",
|
||||
)
|
||||
parser.add_argument("--max-results", type=int, default=100, help="Maximum locations/symbols to print; <=0 disables.")
|
||||
parser.add_argument(
|
||||
"--background-index",
|
||||
action="store_true",
|
||||
help="Allow clangd to build/use its background index for broader cross-translation-unit references.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-incomplete-references",
|
||||
action="store_true",
|
||||
help="Permit current-translation-unit-only references when --background-index is not enabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hierarchical",
|
||||
action="store_true",
|
||||
help="Print nested document symbols instead of the compact flat symbol list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-declaration",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Include declaration in references results.",
|
||||
)
|
||||
return run(parser.parse_args(argv))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
59
skills/panopainter-code-navigation/SKILL.md
Normal file
59
skills/panopainter-code-navigation/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: panopainter-code-navigation
|
||||
description: Use when working in the PanoPainter repository and Codex needs to follow C++ symbols, inspect declarations/definitions/hover info, list matching symbols, use regex symbol/detail/path filters, or reduce broad rg searches during refactors. Prefer this before text search for symbol navigation, regex symbol-family lookup, service-interface wiring, override/implementation lookup, or legacy-to-component boundary tracing.
|
||||
---
|
||||
|
||||
# PanoPainter Code Navigation
|
||||
|
||||
Use the repo's clangd helper before broad text searches when a task depends on C++
|
||||
symbol identity rather than plain text. This is required for PanoPainter C++
|
||||
refactors that depend on symbol families, declarations/definitions, override
|
||||
groups, service/interface wiring, or platform/backend boundaries.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the current workspace is `D:\Dev\panopainter`.
|
||||
2. Use this skill before broad text search when a C++ refactor depends on
|
||||
symbol identity, a symbol family, signatures, override groups, or
|
||||
platform/backend boundary paths.
|
||||
3. Prefer reliable lookups:
|
||||
- `symbols`
|
||||
- `definition`
|
||||
- `declaration`
|
||||
- `implementation`
|
||||
- `hover`
|
||||
4. Use regex filters when looking for symbol families, generated-style names, or
|
||||
backend/platform boundary patterns:
|
||||
- `--name-regex` filters flat symbols by `qualifiedName`.
|
||||
- `--detail-regex` filters flat symbols by detail/signature text.
|
||||
- `--path-regex` filters definition/declaration/implementation/reference
|
||||
locations by path or URI.
|
||||
- Regex matching is case-insensitive by default; add `--no-ignore-case` for
|
||||
case-sensitive checks.
|
||||
- Run `python scripts/dev/clangd_nav.py self-test` if the helper changed or
|
||||
regex output looks surprising.
|
||||
5. Use `references` only as advisory:
|
||||
- Pass `--background-index` for broader best-effort references.
|
||||
- Pass `--allow-incomplete-references` only for explicitly current-translation-unit-only references.
|
||||
- Never treat incomplete reference output as proof that no other users exist.
|
||||
6. Keep output small with `--name`, regex filters, and `--max-results`.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py hover --file src/app_core/brush_ui.h --line 783 --column 60
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
The helper uses `PP_CLANGD_COMPILE_COMMANDS_DIR` when set, otherwise it checks
|
||||
known Ninja build trees such as `out/build/windows-clangcl-asan` and
|
||||
`out/build/android-arm64`. Pass `--compile-commands-dir` when using another
|
||||
configured build tree.
|
||||
|
||||
Use normal `rg` for non-symbol text, docs, build files, generated command names,
|
||||
or when clangd cannot parse the relevant file.
|
||||
4
skills/panopainter-code-navigation/agents/openai.yaml
Normal file
4
skills/panopainter-code-navigation/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "PanoPainter Code Navigation"
|
||||
short_description: "Use clangd navigation for PanoPainter C++ symbols."
|
||||
default_prompt: "Use compiler-aware clangd navigation in PanoPainter before broad text searches when following C++ symbols."
|
||||
1069
src/app.cpp
1069
src/app.cpp
File diff suppressed because it is too large
Load Diff
154
src/app.h
154
src/app.h
@@ -19,30 +19,38 @@
|
||||
#include "node_canvas.h"
|
||||
#include "node_dialog_layer_rename.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "node_panel_quick.h"
|
||||
#include "node_input_box.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "layout.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "app_core/app_thread.h"
|
||||
|
||||
namespace pp::platform {
|
||||
class PlatformServices;
|
||||
struct PlatformStoragePaths;
|
||||
}
|
||||
|
||||
class NodePanelGrid;
|
||||
|
||||
#if defined(__OBJC__) && defined(__IOS__)
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "GameViewController.h"
|
||||
#import "AppDelegate.h"
|
||||
@class GameViewController;
|
||||
@class AppDelegate;
|
||||
#endif
|
||||
|
||||
#if defined(__OBJC__) && defined(__OSX__)
|
||||
#import "main.h"
|
||||
@class View;
|
||||
@class AppOSX;
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "main.h"
|
||||
struct android_app;
|
||||
struct engine;
|
||||
#endif
|
||||
|
||||
#ifdef __LINUX__
|
||||
#include <GLFW/glfw3.h>
|
||||
#if __LINUX__ || __WEB__
|
||||
struct GLFWwindow;
|
||||
#endif
|
||||
|
||||
struct VRController
|
||||
{
|
||||
enum class kButton : uint8_t
|
||||
@@ -155,6 +163,7 @@ public:
|
||||
float display_density = 1.f;
|
||||
float zoom = 1.f;
|
||||
int idle_ms = 100;
|
||||
pp::platform::PlatformServices* platform_services_ = nullptr;
|
||||
|
||||
#if defined(__IOS__) && defined(__OBJC__)
|
||||
GameViewController* ios_view;
|
||||
@@ -174,15 +183,53 @@ public:
|
||||
bool clipboard_set_text(const std::string& s);
|
||||
void pick_image(std::function<void(std::string path)> callback);
|
||||
void pick_file(std::vector<std::string> types, std::function<void(std::string path)> callback);
|
||||
#if __IOS__ || __WEB__
|
||||
void pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string path)> writer, std::function<void(const std::string& path, bool saved)> callback);
|
||||
#else
|
||||
void pick_file_save(std::vector<std::string> types, std::function<void(std::string path)> callback);
|
||||
#endif
|
||||
[[nodiscard]] bool supports_working_directory_picker() const;
|
||||
[[nodiscard]] std::string format_working_directory_path(std::string_view path) const;
|
||||
[[nodiscard]] bool uses_prepared_file_writes() const;
|
||||
[[nodiscard]] bool uses_work_directory_document_export_collections() const;
|
||||
[[nodiscard]] bool disables_network_tls_verification() const;
|
||||
[[nodiscard]] bool uses_ppbr_export_data_directory_override() const;
|
||||
[[nodiscard]] bool platform_supports_sonarpen() const;
|
||||
void start_platform_sonarpen();
|
||||
[[nodiscard]] int default_canvas_resolution() const;
|
||||
[[nodiscard]] bool draws_canvas_tip_for_input(kEventSource source, kEventType type) const;
|
||||
[[nodiscard]] float adjust_canvas_input_pressure(float pressure) const;
|
||||
void pick_dir(std::function<void(std::string path)> callback);
|
||||
void display_file(std::string path);
|
||||
void share_file(std::string path);
|
||||
void request_app_close();
|
||||
[[nodiscard]] bool start_platform_vr_mode();
|
||||
void stop_platform_vr_mode();
|
||||
void attach_ui_thread();
|
||||
void detach_ui_thread();
|
||||
void acquire_render_context();
|
||||
void release_render_context();
|
||||
void present_render_context();
|
||||
void bind_default_render_target();
|
||||
void bind_main_render_target();
|
||||
void apply_render_platform_hints();
|
||||
void install_render_debug_callback();
|
||||
void begin_render_capture_frame();
|
||||
void end_render_capture_frame();
|
||||
[[nodiscard]] bool platform_deletes_recorded_files_on_clear();
|
||||
void clear_platform_recorded_files(std::string path);
|
||||
void publish_exported_image(std::string path);
|
||||
void flush_platform_storage();
|
||||
[[nodiscard]] std::vector<std::string> document_browse_roots() const;
|
||||
void save_platform_ui_state();
|
||||
[[nodiscard]] bool platform_enables_live_asset_reloading();
|
||||
void update_platform_frame(float delta_time_seconds);
|
||||
void report_rendered_frames(int frames);
|
||||
void save_prepared_file(
|
||||
std::string path,
|
||||
std::string suggested_name,
|
||||
std::function<void(const std::string& path, bool saved)> callback);
|
||||
void set_platform_services(pp::platform::PlatformServices* services) noexcept;
|
||||
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
|
||||
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths();
|
||||
void showKeyboard();
|
||||
void hideKeyboard();
|
||||
void initLog();
|
||||
@@ -248,6 +295,8 @@ public:
|
||||
void dialog_usermanual();
|
||||
void dialog_changelog();
|
||||
void dialog_about();
|
||||
void save_document(pp::app::DocumentSaveIntent intent);
|
||||
void continue_document_workflow_after_optional_save(std::function<void()> action);
|
||||
void dialog_newdoc();
|
||||
void dialog_save();
|
||||
void dialog_save_ver();
|
||||
@@ -327,21 +376,36 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
unique,
|
||||
0U,
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
render_tasklist.size(),
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !render_tasklist.empty())
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
render_tasklist.erase(std::remove_if(render_tasklist.begin(), render_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist.end());
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
render_cv.notify_all();
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
@@ -351,19 +415,27 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
false,
|
||||
0U,
|
||||
render_running,
|
||||
true,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
render_cv.notify_all();
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
if (render_running)
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
}
|
||||
|
||||
@@ -399,21 +471,36 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
unique,
|
||||
0U,
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
ui_tasklist.size(),
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !ui_tasklist.empty())
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
ui_tasklist.erase(std::remove_if(ui_tasklist.begin(), ui_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist.end());
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
ui_cv.notify_all();
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
@@ -423,21 +510,30 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
false,
|
||||
0U,
|
||||
ui_running,
|
||||
true,
|
||||
true);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
ui_cv.notify_all();
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
if (ui_running)
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
redraw = true;
|
||||
if (dispatch.request_redraw)
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
void ui_sync()
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/document_cloud.h"
|
||||
#include "legacy_cloud_services.h"
|
||||
#include "util.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "node_dialog_cloud.h"
|
||||
|
||||
void App::cloud_upload()
|
||||
{
|
||||
if (!canvas)
|
||||
return;
|
||||
if (Canvas::I->m_newdoc)
|
||||
{
|
||||
message_box("Warning", "This document needs to be saved before upload.");
|
||||
}
|
||||
else
|
||||
{
|
||||
auto upload_thread = [this] {
|
||||
BT_SetTerminate();
|
||||
const bool has_canvas = canvas != nullptr;
|
||||
const auto plan = pp::app::plan_cloud_upload(
|
||||
has_canvas,
|
||||
has_canvas && Canvas::I->m_newdoc,
|
||||
has_canvas && Canvas::I->m_unsaved);
|
||||
|
||||
if (Canvas::I->m_unsaved)
|
||||
{
|
||||
Canvas::I->project_save_thread(doc_path, true);
|
||||
}
|
||||
|
||||
auto pb = show_progress("Uploading");
|
||||
|
||||
upload(doc_path, doc_filename, [this,pb](float p){
|
||||
pb->set_progress(p);
|
||||
});
|
||||
|
||||
pb->destroy();
|
||||
message_box("Success", "This document has been succesfully uploaded.");
|
||||
};
|
||||
|
||||
auto m = message_box("Publish document", "Would you like to upload to the public domain?");
|
||||
m->btn_ok->m_text->set_text("Yes");
|
||||
m->btn_cancel->m_text->set_text("No");
|
||||
m->btn_ok->on_click = [this, m, upload_thread](Node*) {
|
||||
std::thread(upload_thread).detach();
|
||||
m->destroy();
|
||||
};
|
||||
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
|
||||
m->destroy();
|
||||
};
|
||||
}
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_upload_plan(*this, plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud upload action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void App::cloud_upload_all()
|
||||
@@ -51,70 +23,18 @@ void App::cloud_upload_all()
|
||||
BT_SetTerminate();
|
||||
|
||||
auto names = Asset::list_files(data_path, ".*\\.ppi");
|
||||
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
|
||||
|
||||
gl_state gl;
|
||||
std::shared_ptr<NodeProgressBar> pb;
|
||||
if (layout.m_loaded)
|
||||
pb = show_progress("Export Pano Image", names.size());
|
||||
|
||||
for (const auto& n : names)
|
||||
{
|
||||
std::string path = data_path + "/" + n;
|
||||
upload(path);
|
||||
|
||||
if (layout.m_loaded)
|
||||
pb->increment();
|
||||
}
|
||||
|
||||
if (layout.m_loaded)
|
||||
pb->destroy();
|
||||
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud bulk upload action failed: %s", status.message);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void App::cloud_browse()
|
||||
{
|
||||
if (!canvas)
|
||||
return;
|
||||
|
||||
// load thumbnail test
|
||||
auto dialog = std::make_shared<NodeDialogCloud>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
if (dialog->selected_file.empty())
|
||||
return;
|
||||
dialog->destroy();
|
||||
std::thread([this, dialog] {
|
||||
BT_SetTerminate();
|
||||
|
||||
auto* m = layout[main_id]->add_child<NodeMessageBox>();
|
||||
m->m_title->set_text("Downloading");
|
||||
m->m_message->set_text("Download in progress");
|
||||
std::string url = "https://panopainter.com/cloud/cloud-dwl.php?file=" + dialog->selected_file;
|
||||
download(url, dialog->selected_path, [this,m](float p){
|
||||
static char progress[256];
|
||||
sprintf(progress, "Download in progress %.2f%%", p * 100.f);
|
||||
m->m_message->set_text(progress);
|
||||
});
|
||||
|
||||
canvas->reset_camera();
|
||||
layers->clear();
|
||||
|
||||
canvas->m_canvas->project_open_thread(dialog->selected_path);
|
||||
|
||||
doc_name = dialog->selected_name;
|
||||
title_update();
|
||||
for (auto& l : canvas->m_canvas->m_layers)
|
||||
layers->add_layer(l->m_name.c_str(), false);
|
||||
ActionManager::clear();
|
||||
m->destroy();
|
||||
}).detach();
|
||||
};
|
||||
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_browse_action(*this, browse_plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud browse action failed: %s", status.message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,70 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/command_convert.h"
|
||||
#include "app.h"
|
||||
#include "canvas.h"
|
||||
#include "legacy_ui_gl_dispatch.h"
|
||||
#include "log.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_convert_command_state()
|
||||
{
|
||||
const auto status = pp::renderer::gl::apply_panopainter_convert_command_state(
|
||||
pp::renderer::gl::OpenGlConvertCommandStateDispatch {
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
.blend_func = pp::legacy::ui_gl::set_opengl_blend_func,
|
||||
.blend_equation = pp::legacy::ui_gl::set_opengl_blend_equation,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL convert command state failed: %s", status.message);
|
||||
}
|
||||
|
||||
class LegacyCommandConvertServices final : public pp::app::CommandConvertServices {
|
||||
public:
|
||||
void apply_renderer_state() override
|
||||
{
|
||||
apply_convert_command_state();
|
||||
}
|
||||
|
||||
void create_canvas(int canvas_resolution) override
|
||||
{
|
||||
command_canvas = new Canvas;
|
||||
command_canvas->create(canvas_resolution, canvas_resolution);
|
||||
}
|
||||
|
||||
void open_project(std::string_view project_path) override
|
||||
{
|
||||
if (command_canvas)
|
||||
command_canvas->project_open_thread(std::string(project_path));
|
||||
}
|
||||
|
||||
void export_equirectangular(std::string_view output_path) override
|
||||
{
|
||||
if (command_canvas)
|
||||
command_canvas->export_equirectangular_thread(std::string(output_path));
|
||||
}
|
||||
|
||||
private:
|
||||
Canvas* command_canvas = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void App::cmd_convert(std::string pano_path, std::string out_path)
|
||||
{
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glBlendEquation(GL_FUNC_ADD);
|
||||
const auto plan = pp::app::plan_command_convert(
|
||||
pano_path,
|
||||
out_path,
|
||||
default_canvas_resolution());
|
||||
if (!plan) {
|
||||
LOG("Convert command rejected: %s", plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas* canvas = new Canvas;
|
||||
canvas->create(CANVAS_RES, CANVAS_RES);
|
||||
canvas->project_open_thread(pano_path);
|
||||
canvas->export_equirectangular_thread(out_path);
|
||||
LegacyCommandConvertServices services;
|
||||
const auto status = pp::app::execute_command_convert_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Convert command failed: %s", status.message);
|
||||
}
|
||||
|
||||
126
src/app_core/about_menu.h
Normal file
126
src/app_core/about_menu.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class AboutMenuCommand {
|
||||
help_guide,
|
||||
about_app,
|
||||
whats_new,
|
||||
induce_crash,
|
||||
performance_test,
|
||||
};
|
||||
|
||||
enum class AboutMenuAction {
|
||||
show_user_manual,
|
||||
show_about_dialog,
|
||||
show_whats_new_dialog,
|
||||
trigger_crash_test,
|
||||
run_performance_test,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
struct AboutMenuPlan {
|
||||
AboutMenuCommand command = AboutMenuCommand::help_guide;
|
||||
AboutMenuAction action = AboutMenuAction::show_user_manual;
|
||||
std::string label;
|
||||
bool closes_root_popup = true;
|
||||
bool requires_canvas = false;
|
||||
int performance_iterations = 0;
|
||||
int performance_updates_per_iteration = 0;
|
||||
};
|
||||
|
||||
class AboutMenuServices {
|
||||
public:
|
||||
virtual ~AboutMenuServices() = default;
|
||||
|
||||
virtual void show_user_manual() = 0;
|
||||
virtual void show_about_dialog() = 0;
|
||||
virtual void show_whats_new_dialog() = 0;
|
||||
virtual void trigger_crash_test() = 0;
|
||||
virtual void run_performance_test(const AboutMenuPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AboutMenuPlan plan_about_menu_command(
|
||||
AboutMenuCommand command,
|
||||
int version_major,
|
||||
int version_minor,
|
||||
int version_fix,
|
||||
bool diagnostics_available = true,
|
||||
bool has_canvas = true)
|
||||
{
|
||||
AboutMenuPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case AboutMenuCommand::help_guide:
|
||||
plan.action = AboutMenuAction::show_user_manual;
|
||||
plan.label = "Help Guide";
|
||||
break;
|
||||
case AboutMenuCommand::about_app:
|
||||
plan.action = AboutMenuAction::show_about_dialog;
|
||||
plan.label = "About PanoPainter";
|
||||
break;
|
||||
case AboutMenuCommand::whats_new:
|
||||
plan.action = AboutMenuAction::show_whats_new_dialog;
|
||||
plan.label = "What's new in "
|
||||
+ std::to_string(version_major)
|
||||
+ "."
|
||||
+ std::to_string(version_minor)
|
||||
+ "."
|
||||
+ std::to_string(version_fix)
|
||||
+ "?";
|
||||
break;
|
||||
case AboutMenuCommand::induce_crash:
|
||||
plan.label = "Induce crash";
|
||||
plan.action = diagnostics_available
|
||||
? AboutMenuAction::trigger_crash_test
|
||||
: AboutMenuAction::no_op_unavailable;
|
||||
plan.closes_root_popup = diagnostics_available;
|
||||
break;
|
||||
case AboutMenuCommand::performance_test:
|
||||
plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)";
|
||||
plan.requires_canvas = true;
|
||||
plan.performance_iterations = 100;
|
||||
plan.performance_updates_per_iteration = 10;
|
||||
plan.action = has_canvas
|
||||
? AboutMenuAction::run_performance_test
|
||||
: AboutMenuAction::no_op_unavailable;
|
||||
plan.closes_root_popup = has_canvas;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_about_menu_plan(
|
||||
const AboutMenuPlan& plan,
|
||||
AboutMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case AboutMenuAction::show_user_manual:
|
||||
services.show_user_manual();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::show_about_dialog:
|
||||
services.show_about_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::show_whats_new_dialog:
|
||||
services.show_whats_new_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::trigger_crash_test:
|
||||
services.trigger_crash_test();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::run_performance_test:
|
||||
services.run_performance_test(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown about menu action");
|
||||
}
|
||||
|
||||
}
|
||||
118
src/app_core/app_dialog.h
Normal file
118
src/app_core/app_dialog.h
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class AppDialogKind {
|
||||
progress,
|
||||
message,
|
||||
input,
|
||||
};
|
||||
|
||||
struct AppProgressDialogPlan {
|
||||
std::string title;
|
||||
int total = 0;
|
||||
int count = 0;
|
||||
float progress_fraction = 0.0F;
|
||||
};
|
||||
|
||||
struct AppMessageDialogPlan {
|
||||
std::string title;
|
||||
std::string message;
|
||||
std::string ok_caption = "Ok";
|
||||
std::string cancel_caption = "Cancel";
|
||||
bool show_cancel = false;
|
||||
};
|
||||
|
||||
struct AppInputDialogPlan {
|
||||
std::string title;
|
||||
std::string field_name;
|
||||
std::string ok_caption = "Ok";
|
||||
};
|
||||
|
||||
class AppDialog {
|
||||
public:
|
||||
virtual ~AppDialog() = default;
|
||||
[[nodiscard]] virtual AppDialogKind kind() const noexcept = 0;
|
||||
};
|
||||
|
||||
class AppProgressDialog : public AppDialog {
|
||||
public:
|
||||
~AppProgressDialog() override = default;
|
||||
};
|
||||
|
||||
class AppMessageDialog : public AppDialog {
|
||||
public:
|
||||
~AppMessageDialog() override = default;
|
||||
};
|
||||
|
||||
class AppInputDialog : public AppDialog {
|
||||
public:
|
||||
~AppInputDialog() override = default;
|
||||
};
|
||||
|
||||
class AppDialogFactory {
|
||||
public:
|
||||
virtual ~AppDialogFactory() = default;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppProgressDialog> show_progress_dialog(
|
||||
const AppProgressDialogPlan& plan) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppMessageDialog> show_message_dialog(
|
||||
const AppMessageDialogPlan& plan) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppInputDialog> show_input_dialog(
|
||||
const AppInputDialogPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_app_progress_dialog(
|
||||
std::string_view title,
|
||||
int total) noexcept
|
||||
{
|
||||
return {
|
||||
std::string(title),
|
||||
total < 0 ? 0 : total,
|
||||
0,
|
||||
0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_app_message_dialog(
|
||||
std::string_view title,
|
||||
std::string_view message,
|
||||
bool show_cancel,
|
||||
std::string_view ok_caption = "Ok",
|
||||
std::string_view cancel_caption = "Cancel")
|
||||
{
|
||||
return {
|
||||
std::string(title),
|
||||
std::string(message),
|
||||
std::string(ok_caption),
|
||||
std::string(cancel_caption),
|
||||
show_cancel,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppInputDialogPlan> plan_app_input_dialog(
|
||||
std::string_view title,
|
||||
std::string_view field_name,
|
||||
std::string_view ok_caption)
|
||||
{
|
||||
if (ok_caption.empty()) {
|
||||
return pp::foundation::Result<AppInputDialogPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("input dialog ok caption must not be empty"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppInputDialogPlan>::success({
|
||||
std::string(title),
|
||||
std::string(field_name),
|
||||
std::string(ok_caption),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
240
src/app_core/app_frame.h
Normal file
240
src/app_core/app_frame.h
Normal file
@@ -0,0 +1,240 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppInitialSurfacePlan {
|
||||
float width = 960.0F;
|
||||
float height = 540.0F;
|
||||
};
|
||||
|
||||
struct AppFrameUpdatePlan {
|
||||
bool update_frame = false;
|
||||
bool update_layouts = false;
|
||||
bool refresh_canvas_toolbar = false;
|
||||
};
|
||||
|
||||
struct AppFrameDrawPlan {
|
||||
bool draw_canvas_stroke = false;
|
||||
bool draw_vr_ui = false;
|
||||
bool draw_main_ui = true;
|
||||
bool reset_redraw = true;
|
||||
};
|
||||
|
||||
struct AppFrameTickPlan {
|
||||
bool tick_designer_layout = false;
|
||||
bool tick_main_layout = false;
|
||||
};
|
||||
|
||||
struct AppResizePlan {
|
||||
float width = 0.0F;
|
||||
float height = 0.0F;
|
||||
int render_target_width = 0;
|
||||
int render_target_height = 0;
|
||||
bool recreate_ui_render_target = true;
|
||||
bool request_redraw = true;
|
||||
};
|
||||
|
||||
struct AppUiObserverRect {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
float width = 0.0F;
|
||||
float height = 0.0F;
|
||||
};
|
||||
|
||||
struct AppUiObserverParentClip {
|
||||
AppUiObserverRect clip;
|
||||
float padding_top = 0.0F;
|
||||
float padding_right = 0.0F;
|
||||
float padding_bottom = 0.0F;
|
||||
float padding_left = 0.0F;
|
||||
};
|
||||
|
||||
struct AppUiObserverPlan {
|
||||
bool draw_node = false;
|
||||
bool notify_enter_screen = false;
|
||||
bool notify_leave_screen = false;
|
||||
bool next_on_screen = false;
|
||||
AppUiObserverRect visible_clip;
|
||||
std::int32_t scissor_x = 0;
|
||||
std::int32_t scissor_y = 0;
|
||||
std::int32_t scissor_width = 0;
|
||||
std::int32_t scissor_height = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppInitialSurfacePlan plan_app_initial_surface() noexcept
|
||||
{
|
||||
return AppInitialSurfacePlan {
|
||||
.width = 1920.0F / 2.0F,
|
||||
.height = 1080.0F / 2.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameUpdatePlan plan_app_frame_update(bool redraw, bool animate) noexcept
|
||||
{
|
||||
const bool update_frame = redraw || animate;
|
||||
return AppFrameUpdatePlan {
|
||||
.update_frame = update_frame,
|
||||
.update_layouts = update_frame,
|
||||
.refresh_canvas_toolbar = update_frame,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameDrawPlan plan_app_frame_draw(
|
||||
bool has_canvas_node,
|
||||
bool has_canvas_document,
|
||||
bool vr_active,
|
||||
bool ui_visible,
|
||||
bool vr_only) noexcept
|
||||
{
|
||||
return AppFrameDrawPlan {
|
||||
.draw_canvas_stroke = has_canvas_node && has_canvas_document,
|
||||
.draw_vr_ui = vr_active && ui_visible,
|
||||
.draw_main_ui = !vr_only,
|
||||
.reset_redraw = true,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameTickPlan plan_app_frame_tick(
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout) noexcept
|
||||
{
|
||||
return AppFrameTickPlan {
|
||||
.tick_designer_layout = has_designer_layout,
|
||||
.tick_main_layout = has_main_layout,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppResizePlan> plan_app_resize(float width, float height)
|
||||
{
|
||||
if (!std::isfinite(width) || !std::isfinite(height)) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("resize dimensions must be finite"));
|
||||
}
|
||||
|
||||
if (width < 1.0F || height < 1.0F) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("resize dimensions must be positive"));
|
||||
}
|
||||
|
||||
if (width > static_cast<float>(std::numeric_limits<int>::max())
|
||||
|| height > static_cast<float>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("resize dimensions exceed integer range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppResizePlan>::success(AppResizePlan {
|
||||
.width = width,
|
||||
.height = height,
|
||||
.render_target_width = static_cast<int>(width),
|
||||
.render_target_height = static_cast<int>(height),
|
||||
.recreate_ui_render_target = true,
|
||||
.request_redraw = true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiObserverRect intersect_app_ui_observer_rect(
|
||||
AppUiObserverRect a,
|
||||
AppUiObserverRect b) noexcept
|
||||
{
|
||||
const float x0 = a.x > b.x ? a.x : b.x;
|
||||
const float y0 = a.y > b.y ? a.y : b.y;
|
||||
const float x1 = (a.x + a.width) < (b.x + b.width) ? (a.x + a.width) : (b.x + b.width);
|
||||
const float y1 = (a.y + a.height) < (b.y + b.height) ? (a.y + a.height) : (b.y + b.height);
|
||||
return AppUiObserverRect {
|
||||
.x = x0,
|
||||
.y = y0,
|
||||
.width = x1 - x0,
|
||||
.height = y1 - y0,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiObserverPlan> plan_app_ui_observer(
|
||||
bool has_node,
|
||||
bool display,
|
||||
bool was_on_screen,
|
||||
AppUiObserverRect node_clip,
|
||||
std::span<const AppUiObserverParentClip> parent_clips,
|
||||
float surface_height,
|
||||
float zoom,
|
||||
float offset_x,
|
||||
float offset_y)
|
||||
{
|
||||
if (!has_node || !display) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = false,
|
||||
.next_on_screen = was_on_screen,
|
||||
.visible_clip = node_clip,
|
||||
});
|
||||
}
|
||||
|
||||
const auto finite_rect = [](AppUiObserverRect rect) noexcept {
|
||||
return std::isfinite(rect.x) && std::isfinite(rect.y)
|
||||
&& std::isfinite(rect.width) && std::isfinite(rect.height);
|
||||
};
|
||||
|
||||
if (!finite_rect(node_clip) || !std::isfinite(surface_height)
|
||||
|| !std::isfinite(zoom) || !std::isfinite(offset_x) || !std::isfinite(offset_y)) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer geometry must be finite"));
|
||||
}
|
||||
|
||||
if (surface_height < 1.0F || zoom <= 0.0F) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer surface height and zoom must be positive"));
|
||||
}
|
||||
|
||||
AppUiObserverRect visible = node_clip;
|
||||
for (const auto& parent : parent_clips) {
|
||||
if (!finite_rect(parent.clip)
|
||||
|| !std::isfinite(parent.padding_top)
|
||||
|| !std::isfinite(parent.padding_right)
|
||||
|| !std::isfinite(parent.padding_bottom)
|
||||
|| !std::isfinite(parent.padding_left)) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer parent geometry must be finite"));
|
||||
}
|
||||
|
||||
const AppUiObserverRect padded {
|
||||
.x = parent.clip.x + parent.padding_left,
|
||||
.y = parent.clip.y + parent.padding_top,
|
||||
.width = parent.clip.width - parent.padding_right - parent.padding_left,
|
||||
.height = parent.clip.height - parent.padding_bottom - parent.padding_top,
|
||||
};
|
||||
visible = intersect_app_ui_observer_rect(visible, padded);
|
||||
}
|
||||
|
||||
if (visible.width <= 0.0F || visible.height <= 0.0F) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = false,
|
||||
.notify_leave_screen = was_on_screen,
|
||||
.next_on_screen = false,
|
||||
.visible_clip = visible,
|
||||
});
|
||||
}
|
||||
|
||||
const float projected_x = (visible.x - 1.0F) * zoom;
|
||||
const float projected_y = (surface_height / zoom - visible.y - visible.height - 1.0F) * zoom;
|
||||
const float projected_width = (visible.width + 2.0F) * zoom;
|
||||
const float projected_height = (visible.height + 2.0F) * zoom;
|
||||
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = true,
|
||||
.notify_enter_screen = !was_on_screen,
|
||||
.notify_leave_screen = false,
|
||||
.next_on_screen = true,
|
||||
.visible_clip = visible,
|
||||
.scissor_x = static_cast<std::int32_t>(std::floor(projected_x + offset_x)),
|
||||
.scissor_y = static_cast<std::int32_t>(std::floor(projected_y + offset_y)),
|
||||
.scissor_width = static_cast<std::int32_t>(std::ceil(projected_width)),
|
||||
.scissor_height = static_cast<std::int32_t>(std::ceil(projected_height)),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
209
src/app_core/app_input.h
Normal file
209
src/app_core/app_input.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppPointerDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_designer_first = false;
|
||||
bool dispatch_main_if_not_consumed = false;
|
||||
float normalized_x = 0.0F;
|
||||
float normalized_y = 0.0F;
|
||||
};
|
||||
|
||||
struct AppInputDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
};
|
||||
|
||||
struct AppGestureDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
float normalized_x = 0.0F;
|
||||
float normalized_y = 0.0F;
|
||||
float distance = 0.0F;
|
||||
float distance_delta = 0.0F;
|
||||
float position_delta_x = 0.0F;
|
||||
float position_delta_y = 0.0F;
|
||||
};
|
||||
|
||||
struct AppKeyDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
bool set_key_down = false;
|
||||
bool sync_vr_camera_rotation = false;
|
||||
};
|
||||
|
||||
struct AppUiVisibilityTogglePlan {
|
||||
bool next_ui_visible = true;
|
||||
std::size_t first_panel_child_index = 1;
|
||||
std::size_t panel_child_count = 0;
|
||||
};
|
||||
|
||||
struct AppStylusAttachPlan {
|
||||
bool set_has_stylus = true;
|
||||
bool enable_canvas_touch_lock = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_input_zoom(float zoom)
|
||||
{
|
||||
if (!std::isfinite(zoom) || zoom <= 0.0F) {
|
||||
return pp::foundation::Status::invalid_argument("input zoom must be finite and positive");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppPointerDispatchPlan> plan_app_pointer_dispatch(
|
||||
float x,
|
||||
float y,
|
||||
float zoom,
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout)
|
||||
{
|
||||
const auto zoom_status = validate_input_zoom(zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::failure(zoom_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(x) || !std::isfinite(y)) {
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("input coordinates must be finite"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::success(AppPointerDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_designer_first = has_designer_layout,
|
||||
.dispatch_main_if_not_consumed = has_main_layout,
|
||||
.normalized_x = x / zoom,
|
||||
.normalized_y = y / zoom,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppPointerDispatchPlan plan_app_mouse_cancel_dispatch(
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout) noexcept
|
||||
{
|
||||
return AppPointerDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_designer_first = has_designer_layout,
|
||||
.dispatch_main_if_not_consumed = has_main_layout,
|
||||
.normalized_x = 0.0F,
|
||||
.normalized_y = 0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppInputDispatchPlan plan_app_main_input_dispatch(bool has_main_layout) noexcept
|
||||
{
|
||||
return AppInputDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppGestureDispatchPlan> plan_app_gesture_dispatch(
|
||||
float x0,
|
||||
float y0,
|
||||
float x1,
|
||||
float y1,
|
||||
float previous_x0,
|
||||
float previous_y0,
|
||||
float previous_x1,
|
||||
float previous_y1,
|
||||
float zoom,
|
||||
bool has_main_layout)
|
||||
{
|
||||
const auto zoom_status = validate_input_zoom(zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::failure(zoom_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1)
|
||||
|| !std::isfinite(previous_x0) || !std::isfinite(previous_y0)
|
||||
|| !std::isfinite(previous_x1) || !std::isfinite(previous_y1)) {
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("gesture coordinates must be finite"));
|
||||
}
|
||||
|
||||
const float midpoint_x = (x0 + x1) * 0.5F;
|
||||
const float midpoint_y = (y0 + y1) * 0.5F;
|
||||
const float previous_midpoint_x = (previous_x0 + previous_x1) * 0.5F;
|
||||
const float previous_midpoint_y = (previous_y0 + previous_y1) * 0.5F;
|
||||
const float dx = x1 - x0;
|
||||
const float dy = y1 - y0;
|
||||
const float previous_dx = previous_x1 - previous_x0;
|
||||
const float previous_dy = previous_y1 - previous_y0;
|
||||
const float distance = std::sqrt(dx * dx + dy * dy);
|
||||
const float previous_distance = std::sqrt(previous_dx * previous_dx + previous_dy * previous_dy);
|
||||
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::success(AppGestureDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.normalized_x = midpoint_x / zoom,
|
||||
.normalized_y = midpoint_y / zoom,
|
||||
.distance = distance,
|
||||
.distance_delta = distance - previous_distance,
|
||||
.position_delta_x = midpoint_x - previous_midpoint_x,
|
||||
.position_delta_y = midpoint_y - previous_midpoint_y,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_down_dispatch(
|
||||
bool has_main_layout,
|
||||
bool is_spacebar,
|
||||
bool vr_active) noexcept
|
||||
{
|
||||
return AppKeyDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.set_key_down = true,
|
||||
.sync_vr_camera_rotation = is_spacebar && vr_active,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_up_dispatch(bool has_main_layout) noexcept
|
||||
{
|
||||
return AppKeyDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.set_key_down = false,
|
||||
.sync_vr_camera_rotation = false,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiVisibilityTogglePlan> plan_app_ui_visibility_toggle(
|
||||
bool current_ui_visible,
|
||||
bool has_main_layout,
|
||||
std::size_t main_child_count,
|
||||
std::size_t panel_child_count)
|
||||
{
|
||||
if (!has_main_layout) {
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI toggle requires a main layout"));
|
||||
}
|
||||
|
||||
if (main_child_count <= 1U) {
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI toggle requires a panel container child"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::success(AppUiVisibilityTogglePlan {
|
||||
.next_ui_visible = !current_ui_visible,
|
||||
.first_panel_child_index = 1U,
|
||||
.panel_child_count = panel_child_count,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppStylusAttachPlan plan_app_stylus_attach(bool has_canvas) noexcept
|
||||
{
|
||||
return AppStylusAttachPlan {
|
||||
.set_has_stylus = true,
|
||||
.enable_canvas_touch_lock = has_canvas,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
195
src/app_core/app_preferences.h
Normal file
195
src/app_core/app_preferences.h
Normal file
@@ -0,0 +1,195 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class InterfaceDirection {
|
||||
left_to_right,
|
||||
right_to_left,
|
||||
};
|
||||
|
||||
enum class TimelapseRecordingAction {
|
||||
no_op,
|
||||
start_recording,
|
||||
stop_recording,
|
||||
};
|
||||
|
||||
struct ScaleApplicationPlan {
|
||||
float scale = 1.0F;
|
||||
float display_density = 1.0F;
|
||||
float font_scale = 1.0F;
|
||||
};
|
||||
|
||||
struct ScaleOptionSelection {
|
||||
bool has_selection = false;
|
||||
std::size_t index = 0;
|
||||
};
|
||||
|
||||
struct InterfaceDirectionPlan {
|
||||
InterfaceDirection direction = InterfaceDirection::left_to_right;
|
||||
};
|
||||
|
||||
struct TimelapsePreferencePlan {
|
||||
bool enabled = true;
|
||||
TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op;
|
||||
};
|
||||
|
||||
struct StoredIntegerPreferencePlan {
|
||||
int value = 0;
|
||||
};
|
||||
|
||||
struct StoredBooleanPreferencePlan {
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
class AppPreferenceServices {
|
||||
public:
|
||||
virtual ~AppPreferenceServices() = default;
|
||||
|
||||
virtual void apply_ui_scale(const ScaleApplicationPlan& plan) = 0;
|
||||
virtual void apply_viewport_scale(const ScaleApplicationPlan& plan) = 0;
|
||||
virtual void apply_interface_direction(const InterfaceDirectionPlan& plan) = 0;
|
||||
virtual bool apply_vr_mode_preference(const StoredBooleanPreferencePlan& plan) = 0;
|
||||
virtual void apply_vr_controllers_preference(const StoredBooleanPreferencePlan& plan) = 0;
|
||||
virtual void apply_timelapse_preference(const TimelapsePreferencePlan& plan) = 0;
|
||||
virtual void apply_canvas_cursor_mode(const StoredIntegerPreferencePlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale(
|
||||
float requested_scale,
|
||||
float display_density) noexcept
|
||||
{
|
||||
return {
|
||||
requested_scale,
|
||||
display_density,
|
||||
requested_scale * display_density,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale(
|
||||
float requested_scale,
|
||||
float display_density = 1.0F) noexcept
|
||||
{
|
||||
return {
|
||||
requested_scale,
|
||||
display_density,
|
||||
requested_scale * display_density,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection(
|
||||
float current_scale,
|
||||
std::span<const float> options) noexcept
|
||||
{
|
||||
ScaleOptionSelection selection;
|
||||
for (std::size_t index = 0; index < options.size(); ++index) {
|
||||
if (current_scale >= options[index]) {
|
||||
selection.has_selection = true;
|
||||
selection.index = index;
|
||||
}
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept
|
||||
{
|
||||
return {
|
||||
right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference(
|
||||
bool enabled,
|
||||
bool recording_running) noexcept
|
||||
{
|
||||
if (enabled && !recording_running) {
|
||||
return { enabled, TimelapseRecordingAction::start_recording };
|
||||
}
|
||||
if (!enabled && recording_running) {
|
||||
return { enabled, TimelapseRecordingAction::stop_recording };
|
||||
}
|
||||
return { enabled, TimelapseRecordingAction::no_op };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference(
|
||||
bool enabled) noexcept
|
||||
{
|
||||
return { enabled };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_mode_preference(bool enabled) noexcept
|
||||
{
|
||||
return { enabled };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
|
||||
{
|
||||
return { mode };
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_ui_scale_preference(
|
||||
float requested_scale,
|
||||
float display_density,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_ui_scale(plan_ui_scale(requested_scale, display_density));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_viewport_scale_preference(
|
||||
float requested_scale,
|
||||
float display_density,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_viewport_scale(plan_viewport_scale(requested_scale, display_density));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_interface_direction_preference(
|
||||
bool right_to_left,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_interface_direction(plan_interface_direction(right_to_left));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_vr_mode_preference(
|
||||
bool enabled,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
if (!services.apply_vr_mode_preference(plan_vr_mode_preference(enabled))) {
|
||||
return pp::foundation::Status::invalid_argument("VR mode could not start");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_vr_controllers_preference(
|
||||
bool enabled,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_vr_controllers_preference(plan_vr_controllers_preference(enabled));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_timelapse_preference(
|
||||
bool enabled,
|
||||
bool recording_running,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_timelapse_preference(plan_timelapse_preference(enabled, recording_running));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_cursor_mode_preference(
|
||||
int mode,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_canvas_cursor_mode(plan_canvas_cursor_mode(mode));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
23
src/app_core/app_shutdown.h
Normal file
23
src/app_core/app_shutdown.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppShutdownPlan {
|
||||
bool save_ui_state = true;
|
||||
bool terminate_stroke_preview_renderer = true;
|
||||
bool stop_recording = true;
|
||||
bool invalidate_textures = true;
|
||||
bool invalidate_shaders = true;
|
||||
bool unload_layouts = true;
|
||||
bool destroy_ui_render_target = true;
|
||||
bool destroy_face_plane = true;
|
||||
bool release_panel_nodes = true;
|
||||
bool clear_quick_mode_state = true;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppShutdownPlan plan_app_shutdown() noexcept
|
||||
{
|
||||
return AppShutdownPlan {};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
188
src/app_core/app_startup.h
Normal file
188
src/app_core/app_startup.h
Normal file
@@ -0,0 +1,188 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppStartupPlan {
|
||||
int previous_run_counter = 0;
|
||||
int next_run_counter = 1;
|
||||
bool save_preferences = true;
|
||||
bool start_timelapse = false;
|
||||
bool vr_controllers_enabled = true;
|
||||
bool show_license_warning = false;
|
||||
};
|
||||
|
||||
struct AppStartupResourcePlan {
|
||||
int ui_render_target_width = 0;
|
||||
int ui_render_target_height = 0;
|
||||
bool initialize_shaders = true;
|
||||
bool initialize_assets = true;
|
||||
bool initialize_layout = true;
|
||||
bool update_title = true;
|
||||
bool create_ui_render_target = true;
|
||||
};
|
||||
|
||||
class AppStartupServices {
|
||||
public:
|
||||
virtual ~AppStartupServices() = default;
|
||||
|
||||
virtual void store_run_counter(int value) = 0;
|
||||
virtual void save_preferences() = 0;
|
||||
virtual void start_timelapse_recording() = 0;
|
||||
virtual void apply_vr_controllers_enabled(bool enabled) = 0;
|
||||
virtual void show_license_warning() = 0;
|
||||
};
|
||||
|
||||
class AppStartupResourceServices {
|
||||
public:
|
||||
virtual ~AppStartupResourceServices() = default;
|
||||
|
||||
virtual void initialize_shaders() = 0;
|
||||
virtual void initialize_assets() = 0;
|
||||
virtual void initialize_layout() = 0;
|
||||
virtual void update_title() = 0;
|
||||
virtual void create_ui_render_target(int width, int height) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppStartupPlan> plan_app_startup(
|
||||
int current_run_counter,
|
||||
bool auto_timelapse_enabled,
|
||||
bool stored_vr_controllers_enabled,
|
||||
bool license_valid)
|
||||
{
|
||||
if (current_run_counter < 0) {
|
||||
return pp::foundation::Result<AppStartupPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("run counter must not be negative"));
|
||||
}
|
||||
|
||||
if (current_run_counter == std::numeric_limits<int>::max()) {
|
||||
return pp::foundation::Result<AppStartupPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("run counter would overflow"));
|
||||
}
|
||||
|
||||
AppStartupPlan plan;
|
||||
plan.previous_run_counter = current_run_counter;
|
||||
plan.next_run_counter = current_run_counter + 1;
|
||||
plan.start_timelapse = auto_timelapse_enabled;
|
||||
plan.vr_controllers_enabled = stored_vr_controllers_enabled;
|
||||
plan.show_license_warning = !license_valid;
|
||||
return pp::foundation::Result<AppStartupPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppStartupResourcePlan> plan_app_startup_resources(
|
||||
float ui_width,
|
||||
float ui_height)
|
||||
{
|
||||
if (!std::isfinite(ui_width) || !std::isfinite(ui_height)) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("startup resource dimensions must be finite"));
|
||||
}
|
||||
|
||||
if (ui_width < 1.0F || ui_height < 1.0F) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("startup resource dimensions must be positive"));
|
||||
}
|
||||
|
||||
if (ui_width > static_cast<float>(std::numeric_limits<int>::max())
|
||||
|| ui_height > static_cast<float>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("startup resource dimensions exceed integer range"));
|
||||
}
|
||||
|
||||
AppStartupResourcePlan plan;
|
||||
plan.ui_render_target_width = static_cast<int>(ui_width);
|
||||
plan.ui_render_target_height = static_cast<int>(ui_height);
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
services.store_run_counter(plan.next_run_counter);
|
||||
if (plan.save_preferences) {
|
||||
services.save_preferences();
|
||||
}
|
||||
if (plan.start_timelapse) {
|
||||
services.start_timelapse_recording();
|
||||
}
|
||||
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
|
||||
if (plan.show_license_warning) {
|
||||
services.show_license_warning();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_persistence_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
services.store_run_counter(plan.next_run_counter);
|
||||
if (plan.save_preferences) {
|
||||
services.save_preferences();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_runtime_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
if (plan.start_timelapse) {
|
||||
services.start_timelapse_recording();
|
||||
}
|
||||
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
|
||||
if (plan.show_license_warning) {
|
||||
services.show_license_warning();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_resources(
|
||||
const AppStartupResourcePlan& plan,
|
||||
AppStartupResourceServices& services)
|
||||
{
|
||||
if (plan.create_ui_render_target
|
||||
&& (plan.ui_render_target_width <= 0 || plan.ui_render_target_height <= 0)) {
|
||||
return pp::foundation::Status::invalid_argument("startup resource plan has invalid UI render target size");
|
||||
}
|
||||
|
||||
if (plan.initialize_shaders) {
|
||||
services.initialize_shaders();
|
||||
}
|
||||
if (plan.initialize_assets) {
|
||||
services.initialize_assets();
|
||||
}
|
||||
if (plan.initialize_layout) {
|
||||
services.initialize_layout();
|
||||
}
|
||||
if (plan.update_title) {
|
||||
services.update_title();
|
||||
}
|
||||
if (plan.create_ui_render_target) {
|
||||
services.create_ui_render_target(plan.ui_render_target_width, plan.ui_render_target_height);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
170
src/app_core/app_status.h
Normal file
170
src/app_core/app_status.h
Normal file
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr std::array<int, 6> document_resolution_values {
|
||||
512,
|
||||
1024,
|
||||
1536,
|
||||
2048,
|
||||
4096,
|
||||
8192,
|
||||
};
|
||||
|
||||
inline constexpr std::array<std::string_view, 6> document_resolution_labels {
|
||||
"2K",
|
||||
"4K",
|
||||
"6K",
|
||||
"8K",
|
||||
"16K",
|
||||
"32K",
|
||||
};
|
||||
|
||||
struct RecordingFrameLabel {
|
||||
bool visible = false;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticsInput {
|
||||
bool framebuffer_fetch = false;
|
||||
bool float32_render_targets = false;
|
||||
bool float32_linear_filtering = false;
|
||||
bool float16_render_targets = false;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticIndicator {
|
||||
bool supported = false;
|
||||
std::string_view label;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticsPlan {
|
||||
RendererDiagnosticIndicator framebuffer_fetch;
|
||||
RendererDiagnosticIndicator floating_point_targets;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<int> display_resolution_from_index(int index)
|
||||
{
|
||||
if (index < 0 || static_cast<std::size_t>(index) >= document_resolution_values.size()) {
|
||||
return pp::foundation::Result<int>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||
}
|
||||
return pp::foundation::Result<int>::success(
|
||||
document_resolution_values[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<std::size_t> document_resolution_to_index(int resolution)
|
||||
{
|
||||
for (std::size_t index = 0; index < document_resolution_values.size(); ++index) {
|
||||
if (document_resolution_values[index] == resolution) {
|
||||
return pp::foundation::Result<std::size_t>::success(index);
|
||||
}
|
||||
}
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution is not supported"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<std::string_view> document_resolution_label(int resolution)
|
||||
{
|
||||
const auto index = document_resolution_to_index(resolution);
|
||||
if (!index) {
|
||||
return pp::foundation::Result<std::string_view>::failure(index.status());
|
||||
}
|
||||
return pp::foundation::Result<std::string_view>::success(document_resolution_labels[index.value()]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_document_title(
|
||||
std::string_view document_name,
|
||||
bool has_unsaved_changes,
|
||||
int resolution)
|
||||
{
|
||||
const auto label = document_resolution_label(resolution);
|
||||
const auto resolution_label = label ? label.value() : std::string_view("unknown");
|
||||
std::string title = "Panodoc: ";
|
||||
title.append(document_name);
|
||||
if (has_unsaved_changes) {
|
||||
title.push_back('*');
|
||||
}
|
||||
title.append(" (");
|
||||
title.append(resolution_label);
|
||||
title.push_back(')');
|
||||
return title;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_dpi_label(float zoom)
|
||||
{
|
||||
char buffer[64] {};
|
||||
std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes)
|
||||
{
|
||||
char buffer[128] {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"History memory: %.2f Mb",
|
||||
static_cast<double>(bytes) / 1024.0 / 1024.0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label(
|
||||
bool is_recording,
|
||||
bool encoder_available,
|
||||
int encoded_frames)
|
||||
{
|
||||
if (!is_recording || !encoder_available) {
|
||||
return {};
|
||||
}
|
||||
|
||||
char buffer[128] {};
|
||||
std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames);
|
||||
return {
|
||||
true,
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline RendererDiagnosticsPlan plan_renderer_diagnostics(
|
||||
RendererDiagnosticsInput input) noexcept
|
||||
{
|
||||
RendererDiagnosticsPlan plan;
|
||||
plan.framebuffer_fetch = {
|
||||
input.framebuffer_fetch,
|
||||
"FBF",
|
||||
};
|
||||
|
||||
if (input.float32_linear_filtering) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F32L",
|
||||
};
|
||||
} else if (input.float32_render_targets) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F32",
|
||||
};
|
||||
} else if (input.float16_render_targets) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F16",
|
||||
};
|
||||
} else {
|
||||
plan.floating_point_targets = {
|
||||
false,
|
||||
"",
|
||||
};
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
}
|
||||
200
src/app_core/app_thread.h
Normal file
200
src/app_core/app_thread.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppTaskDispatchPlan {
|
||||
bool execute_immediately = false;
|
||||
bool queue_task = false;
|
||||
bool remove_matching_unique_task = false;
|
||||
bool notify_worker = false;
|
||||
bool wait_for_completion = false;
|
||||
bool request_redraw = false;
|
||||
};
|
||||
|
||||
struct AppAsyncRedrawPlan {
|
||||
bool set_redraw = true;
|
||||
bool notify_ui = true;
|
||||
};
|
||||
|
||||
struct AppQueueDrainPlan {
|
||||
bool mark_running = true;
|
||||
bool drain_tasks = false;
|
||||
bool wrap_in_render_context = false;
|
||||
std::size_t task_count = 0;
|
||||
};
|
||||
|
||||
struct AppUiTickPlan {
|
||||
bool mark_running = true;
|
||||
bool execute_tasks = false;
|
||||
bool tick_app = true;
|
||||
bool update_before_render = false;
|
||||
bool enqueue_render_frame = false;
|
||||
std::size_t task_count = 0;
|
||||
};
|
||||
|
||||
struct AppUiLoopTimerPlan {
|
||||
bool update_platform_frame = true;
|
||||
float frame_accumulator = 0.0F;
|
||||
float fps_accumulator = 0.0F;
|
||||
float reload_accumulator = 0.0F;
|
||||
bool report_rendered_frames = false;
|
||||
int reported_frame_count = 0;
|
||||
int rendered_frames_after_report = 0;
|
||||
bool check_live_asset_reload = false;
|
||||
};
|
||||
|
||||
struct AppUiLoopRedrawPlan {
|
||||
bool tick_app = true;
|
||||
bool update_before_render = false;
|
||||
bool enqueue_render_frame = false;
|
||||
bool reset_frame_accumulator = false;
|
||||
int rendered_frames = 0;
|
||||
};
|
||||
|
||||
struct AppThreadStartPlan {
|
||||
bool start_thread = true;
|
||||
bool mark_running = true;
|
||||
};
|
||||
|
||||
struct AppThreadStopPlan {
|
||||
bool mark_not_running = true;
|
||||
bool notify_worker = true;
|
||||
bool join_thread = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppTaskDispatchPlan plan_app_task_dispatch(
|
||||
bool already_on_target_thread,
|
||||
bool unique,
|
||||
std::size_t queued_task_count,
|
||||
bool worker_running,
|
||||
bool wait_for_completion,
|
||||
bool request_redraw_after_dispatch) noexcept
|
||||
{
|
||||
const bool queue_task = !already_on_target_thread;
|
||||
return AppTaskDispatchPlan {
|
||||
.execute_immediately = already_on_target_thread,
|
||||
.queue_task = queue_task,
|
||||
.remove_matching_unique_task = queue_task && unique && queued_task_count > 0U,
|
||||
.notify_worker = queue_task,
|
||||
.wait_for_completion = queue_task && worker_running && wait_for_completion,
|
||||
.request_redraw = request_redraw_after_dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept
|
||||
{
|
||||
return AppAsyncRedrawPlan {};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_render_queue_drain(std::size_t queued_task_count) noexcept
|
||||
{
|
||||
const bool drain = queued_task_count > 0U;
|
||||
return AppQueueDrainPlan {
|
||||
.mark_running = true,
|
||||
.drain_tasks = drain,
|
||||
.wrap_in_render_context = drain,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_ui_queue_drain(std::size_t queued_task_count) noexcept
|
||||
{
|
||||
return AppQueueDrainPlan {
|
||||
.mark_running = true,
|
||||
.drain_tasks = queued_task_count > 0U,
|
||||
.wrap_in_render_context = false,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiTickPlan plan_app_ui_thread_tick(
|
||||
std::size_t queued_task_count,
|
||||
bool redraw) noexcept
|
||||
{
|
||||
return AppUiTickPlan {
|
||||
.mark_running = true,
|
||||
.execute_tasks = queued_task_count > 0U,
|
||||
.tick_app = true,
|
||||
.update_before_render = redraw,
|
||||
.enqueue_render_frame = redraw,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiLoopTimerPlan> plan_app_ui_loop_timers(
|
||||
float delta_time_seconds,
|
||||
float frame_accumulator,
|
||||
float fps_accumulator,
|
||||
float reload_accumulator,
|
||||
int rendered_frames,
|
||||
bool live_asset_reloading_enabled)
|
||||
{
|
||||
if (!std::isfinite(delta_time_seconds) || !std::isfinite(frame_accumulator)
|
||||
|| !std::isfinite(fps_accumulator) || !std::isfinite(reload_accumulator)) {
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI loop timer values must be finite"));
|
||||
}
|
||||
|
||||
if (delta_time_seconds < 0.0F || frame_accumulator < 0.0F
|
||||
|| fps_accumulator < 0.0F || reload_accumulator < 0.0F || rendered_frames < 0) {
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI loop timer values must not be negative"));
|
||||
}
|
||||
|
||||
AppUiLoopTimerPlan plan;
|
||||
plan.frame_accumulator = frame_accumulator + delta_time_seconds;
|
||||
plan.fps_accumulator = fps_accumulator + delta_time_seconds;
|
||||
plan.reload_accumulator = reload_accumulator;
|
||||
plan.rendered_frames_after_report = rendered_frames;
|
||||
|
||||
if (plan.fps_accumulator > 1.0F) {
|
||||
plan.report_rendered_frames = true;
|
||||
plan.reported_frame_count = rendered_frames;
|
||||
plan.fps_accumulator = 0.0F;
|
||||
plan.rendered_frames_after_report = 0;
|
||||
}
|
||||
|
||||
if (live_asset_reloading_enabled) {
|
||||
plan.reload_accumulator += delta_time_seconds;
|
||||
if (plan.reload_accumulator > 1.0F) {
|
||||
plan.reload_accumulator = 0.0F;
|
||||
plan.check_live_asset_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiLoopRedrawPlan plan_app_ui_loop_redraw(
|
||||
bool redraw,
|
||||
int rendered_frames) noexcept
|
||||
{
|
||||
return AppUiLoopRedrawPlan {
|
||||
.tick_app = true,
|
||||
.update_before_render = redraw,
|
||||
.enqueue_render_frame = redraw,
|
||||
.reset_frame_accumulator = redraw,
|
||||
.rendered_frames = rendered_frames + (redraw ? 1 : 0),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppThreadStartPlan plan_app_thread_start() noexcept
|
||||
{
|
||||
return AppThreadStartPlan {};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppThreadStopPlan plan_app_thread_stop(bool thread_joinable) noexcept
|
||||
{
|
||||
return AppThreadStopPlan {
|
||||
.mark_not_running = true,
|
||||
.notify_worker = true,
|
||||
.join_thread = thread_joinable,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
71
src/app_core/brush_package_export.h
Normal file
71
src/app_core/brush_package_export.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct BrushPackageExportRequest {
|
||||
std::string author;
|
||||
std::string email;
|
||||
std::string url;
|
||||
std::string description;
|
||||
std::string destination_path;
|
||||
bool export_data = false;
|
||||
bool has_header_image = false;
|
||||
};
|
||||
|
||||
class BrushPackageExportServices {
|
||||
public:
|
||||
virtual ~BrushPackageExportServices() = default;
|
||||
|
||||
virtual void export_brush_package(std::string_view path, const BrushPackageExportRequest& request) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_export_path(std::string_view path) noexcept
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("brush package export path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_brush_package_export_success_dialog(std::string_view path)
|
||||
{
|
||||
std::string message = "Brushes exported to:\n";
|
||||
message += path;
|
||||
return plan_app_message_dialog("Export PPBR", message, false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_export_request(
|
||||
std::string_view path,
|
||||
const BrushPackageExportRequest& request) noexcept
|
||||
{
|
||||
(void)request;
|
||||
const auto path_status = validate_brush_package_export_path(path);
|
||||
if (!path_status.ok()) {
|
||||
return path_status;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_package_export(
|
||||
std::string_view path,
|
||||
const BrushPackageExportRequest& request,
|
||||
BrushPackageExportServices& services)
|
||||
{
|
||||
const auto status = validate_brush_package_export_request(path, request);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
services.export_brush_package(path, request);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
56
src/app_core/brush_package_import.h
Normal file
56
src/app_core/brush_package_import.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class BrushPackageImportKind {
|
||||
abr,
|
||||
ppbr,
|
||||
};
|
||||
|
||||
class BrushPackageImportServices {
|
||||
public:
|
||||
virtual ~BrushPackageImportServices() = default;
|
||||
|
||||
virtual void import_brush_package(BrushPackageImportKind kind, std::string_view path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline const char* brush_package_import_kind_name(BrushPackageImportKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case BrushPackageImportKind::abr:
|
||||
return "abr";
|
||||
case BrushPackageImportKind::ppbr:
|
||||
return "ppbr";
|
||||
}
|
||||
|
||||
return "abr";
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_import_path(std::string_view path) noexcept
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("brush package import path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_package_import(
|
||||
BrushPackageImportKind kind,
|
||||
std::string_view path,
|
||||
BrushPackageImportServices& services)
|
||||
{
|
||||
const auto status = validate_brush_package_import_path(path);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
services.import_brush_package(kind, path);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
1008
src/app_core/brush_ui.h
Normal file
1008
src/app_core/brush_ui.h
Normal file
File diff suppressed because it is too large
Load Diff
225
src/app_core/canvas_hotkey.h
Normal file
225
src/app_core/canvas_hotkey.h
Normal file
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/canvas_tool_ui.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasHotkeyEvent {
|
||||
key_down,
|
||||
key_up,
|
||||
touch_tap,
|
||||
};
|
||||
|
||||
enum class CanvasHotkeyKey {
|
||||
other,
|
||||
android_back,
|
||||
alt,
|
||||
e,
|
||||
s,
|
||||
tab,
|
||||
z,
|
||||
bracket_left,
|
||||
bracket_right,
|
||||
};
|
||||
|
||||
enum class CanvasHotkeyAction {
|
||||
none,
|
||||
select_tool,
|
||||
history,
|
||||
save_document,
|
||||
toggle_ui,
|
||||
adjust_brush_size,
|
||||
show_cursor,
|
||||
};
|
||||
|
||||
struct CanvasHotkeyState {
|
||||
bool ctrl_down = false;
|
||||
bool shift_down = false;
|
||||
bool mouse_focused = false;
|
||||
int undo_count = 0;
|
||||
int redo_count = 0;
|
||||
int touch_finger_count = 0;
|
||||
};
|
||||
|
||||
struct CanvasHotkeyPlan {
|
||||
CanvasHotkeyAction action = CanvasHotkeyAction::none;
|
||||
CanvasHotkeyEvent event = CanvasHotkeyEvent::key_up;
|
||||
CanvasHotkeyKey key = CanvasHotkeyKey::other;
|
||||
CanvasToolPlan tool;
|
||||
HistoryUiPlan history;
|
||||
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
|
||||
float brush_size_delta = 0.0F;
|
||||
bool no_op = true;
|
||||
};
|
||||
|
||||
class CanvasHotkeyServices {
|
||||
public:
|
||||
virtual ~CanvasHotkeyServices() = default;
|
||||
|
||||
virtual pp::foundation::Status execute_tool(const CanvasToolPlan& plan) = 0;
|
||||
virtual pp::foundation::Status execute_history(const HistoryUiPlan& plan) = 0;
|
||||
virtual void save_document(DocumentSaveIntent intent) = 0;
|
||||
virtual void toggle_ui() = 0;
|
||||
virtual void adjust_brush_size(float delta) = 0;
|
||||
virtual void show_cursor() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_canvas_hotkey_state(
|
||||
const CanvasHotkeyState& state) noexcept
|
||||
{
|
||||
if (state.undo_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("undo action count must not be negative");
|
||||
}
|
||||
if (state.redo_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("redo action count must not be negative");
|
||||
}
|
||||
if (state.touch_finger_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("touch finger count must not be negative");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasHotkeyPlan> plan_canvas_hotkey(
|
||||
CanvasHotkeyEvent event,
|
||||
CanvasHotkeyKey key,
|
||||
const CanvasHotkeyState& state)
|
||||
{
|
||||
const auto state_status = validate_canvas_hotkey_state(state);
|
||||
if (!state_status.ok()) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(state_status);
|
||||
}
|
||||
|
||||
CanvasHotkeyPlan plan;
|
||||
plan.event = event;
|
||||
plan.key = key;
|
||||
|
||||
if (event == CanvasHotkeyEvent::touch_tap) {
|
||||
if (state.touch_finger_count == 2) {
|
||||
auto history = plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
}
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
if (event == CanvasHotkeyEvent::key_down) {
|
||||
switch (key) {
|
||||
case CanvasHotkeyKey::e:
|
||||
plan.action = CanvasHotkeyAction::select_tool;
|
||||
plan.tool = plan_canvas_tool_select(CanvasToolMode::erase);
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::android_back: {
|
||||
auto history = plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
break;
|
||||
}
|
||||
case CanvasHotkeyKey::alt:
|
||||
if (state.mouse_focused) {
|
||||
plan.action = CanvasHotkeyAction::show_cursor;
|
||||
plan.no_op = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case CanvasHotkeyKey::e:
|
||||
plan.action = CanvasHotkeyAction::select_tool;
|
||||
plan.tool = plan_canvas_tool_select(CanvasToolMode::draw);
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::tab:
|
||||
plan.action = CanvasHotkeyAction::toggle_ui;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::z:
|
||||
if (state.ctrl_down) {
|
||||
auto history = state.shift_down
|
||||
? plan_history_redo(state.redo_count)
|
||||
: plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
}
|
||||
break;
|
||||
case CanvasHotkeyKey::s:
|
||||
if (state.ctrl_down) {
|
||||
plan.action = CanvasHotkeyAction::save_document;
|
||||
plan.save_intent = state.shift_down
|
||||
? DocumentSaveIntent::save_dirty_version
|
||||
: DocumentSaveIntent::save;
|
||||
plan.no_op = false;
|
||||
}
|
||||
break;
|
||||
case CanvasHotkeyKey::bracket_left:
|
||||
plan.action = CanvasHotkeyAction::adjust_brush_size;
|
||||
plan.brush_size_delta = -0.05F;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::bracket_right:
|
||||
plan.action = CanvasHotkeyAction::adjust_brush_size;
|
||||
plan.brush_size_delta = 0.05F;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_hotkey_plan(
|
||||
const CanvasHotkeyPlan& plan,
|
||||
CanvasHotkeyServices& services)
|
||||
{
|
||||
if (plan.no_op || plan.action == CanvasHotkeyAction::none) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
switch (plan.action) {
|
||||
case CanvasHotkeyAction::select_tool:
|
||||
return services.execute_tool(plan.tool);
|
||||
case CanvasHotkeyAction::history:
|
||||
return services.execute_history(plan.history);
|
||||
case CanvasHotkeyAction::save_document:
|
||||
services.save_document(plan.save_intent);
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::toggle_ui:
|
||||
services.toggle_ui();
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::adjust_brush_size:
|
||||
if (plan.brush_size_delta == 0.0F) {
|
||||
return pp::foundation::Status::invalid_argument("brush-size hotkey plan must include a delta");
|
||||
}
|
||||
services.adjust_brush_size(plan.brush_size_delta);
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::show_cursor:
|
||||
services.show_cursor();
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::none:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown canvas hotkey action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
315
src/app_core/canvas_tool_ui.h
Normal file
315
src/app_core/canvas_tool_ui.h
Normal file
@@ -0,0 +1,315 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasToolOperation {
|
||||
select_mode,
|
||||
toggle_picking,
|
||||
toggle_touch_lock,
|
||||
};
|
||||
|
||||
enum class CanvasToolMode {
|
||||
draw,
|
||||
erase,
|
||||
line,
|
||||
camera,
|
||||
grid,
|
||||
copy,
|
||||
cut,
|
||||
fill,
|
||||
mask_free,
|
||||
mask_line,
|
||||
flood_fill,
|
||||
};
|
||||
|
||||
enum class CanvasToolTransformAction {
|
||||
none,
|
||||
copy,
|
||||
cut,
|
||||
};
|
||||
|
||||
enum class CanvasToolToolbarAction {
|
||||
select_mode,
|
||||
toggle_picking,
|
||||
toggle_touch_lock,
|
||||
};
|
||||
|
||||
enum class CanvasCursorVisibilityMode {
|
||||
never,
|
||||
small_brush,
|
||||
not_painting,
|
||||
always,
|
||||
};
|
||||
|
||||
struct CanvasToolPlan {
|
||||
CanvasToolOperation operation = CanvasToolOperation::select_mode;
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
CanvasToolTransformAction transform_action = CanvasToolTransformAction::none;
|
||||
bool selects_toolbar_button = false;
|
||||
bool updates_canvas_mode = false;
|
||||
bool toggles_picking = false;
|
||||
bool toggles_touch_lock = false;
|
||||
bool requires_draw_mode = false;
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
struct CanvasToolButtonState {
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
bool pick_active = false;
|
||||
bool touch_lock_active = false;
|
||||
bool pen_active = false;
|
||||
bool erase_active = false;
|
||||
bool line_active = false;
|
||||
bool camera_active = false;
|
||||
bool grid_active = false;
|
||||
bool copy_active = false;
|
||||
bool cut_active = false;
|
||||
bool fill_active = false;
|
||||
bool mask_free_active = false;
|
||||
bool mask_line_active = false;
|
||||
bool flood_fill_active = false;
|
||||
};
|
||||
|
||||
struct CanvasToolToolbarBinding {
|
||||
std::string_view button_id;
|
||||
CanvasToolToolbarAction action = CanvasToolToolbarAction::select_mode;
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
bool custom_button = true;
|
||||
bool applies_default_on_init = false;
|
||||
};
|
||||
|
||||
struct CanvasToolToolbarPlan {
|
||||
std::array<CanvasToolToolbarBinding, 13> bindings {};
|
||||
CanvasToolMode default_mode = CanvasToolMode::draw;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityInput {
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never;
|
||||
bool has_current_brush = true;
|
||||
float brush_tip_size = 0.0F;
|
||||
bool pen_is_drawing = false;
|
||||
bool alt_down = false;
|
||||
bool pen_is_resizing = false;
|
||||
bool pen_is_picking = false;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityPlan {
|
||||
bool visible = true;
|
||||
bool paint_mode = false;
|
||||
bool uses_brush_size = false;
|
||||
bool uses_pen_state = false;
|
||||
bool forced_visible_by_modifier_or_tool = false;
|
||||
};
|
||||
|
||||
class CanvasToolServices {
|
||||
public:
|
||||
virtual ~CanvasToolServices() = default;
|
||||
|
||||
virtual void select_toolbar_button(CanvasToolMode mode) = 0;
|
||||
virtual void set_transform_action(CanvasToolTransformAction action) = 0;
|
||||
virtual void set_canvas_mode(CanvasToolMode mode) = 0;
|
||||
virtual void toggle_picking() = 0;
|
||||
virtual void toggle_touch_lock() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept
|
||||
{
|
||||
if (mode == CanvasToolMode::copy) {
|
||||
return CanvasToolTransformAction::copy;
|
||||
}
|
||||
if (mode == CanvasToolMode::cut) {
|
||||
return CanvasToolTransformAction::cut;
|
||||
}
|
||||
return CanvasToolTransformAction::none;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::select_mode;
|
||||
plan.mode = mode;
|
||||
plan.transform_action = transform_action_for_mode(mode);
|
||||
plan.selects_toolbar_button = true;
|
||||
plan.updates_canvas_mode = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::toggle_picking;
|
||||
plan.mode = CanvasToolMode::draw;
|
||||
plan.requires_draw_mode = true;
|
||||
plan.toggles_picking = current_mode_is_draw;
|
||||
plan.no_op = !current_mode_is_draw;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::toggle_touch_lock;
|
||||
plan.toggles_touch_lock = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolToolbarPlan plan_canvas_tool_toolbar() noexcept
|
||||
{
|
||||
return {
|
||||
std::array<CanvasToolToolbarBinding, 13> {
|
||||
CanvasToolToolbarBinding { "btn-pen", CanvasToolToolbarAction::select_mode, CanvasToolMode::draw, true, true },
|
||||
CanvasToolToolbarBinding { "btn-pick", CanvasToolToolbarAction::toggle_picking, CanvasToolMode::draw, true, false },
|
||||
CanvasToolToolbarBinding { "btn-touchlock", CanvasToolToolbarAction::toggle_touch_lock, CanvasToolMode::draw, true, false },
|
||||
CanvasToolToolbarBinding { "btn-erase", CanvasToolToolbarAction::select_mode, CanvasToolMode::erase, true, false },
|
||||
CanvasToolToolbarBinding { "btn-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::line, true, false },
|
||||
CanvasToolToolbarBinding { "btn-cam", CanvasToolToolbarAction::select_mode, CanvasToolMode::camera, false, false },
|
||||
CanvasToolToolbarBinding { "btn-grid", CanvasToolToolbarAction::select_mode, CanvasToolMode::grid, false, false },
|
||||
CanvasToolToolbarBinding { "btn-copy", CanvasToolToolbarAction::select_mode, CanvasToolMode::copy, false, false },
|
||||
CanvasToolToolbarBinding { "btn-cut", CanvasToolToolbarAction::select_mode, CanvasToolMode::cut, false, false },
|
||||
CanvasToolToolbarBinding { "btn-fill", CanvasToolToolbarAction::select_mode, CanvasToolMode::fill, false, false },
|
||||
CanvasToolToolbarBinding { "btn-mask-free", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_free, true, false },
|
||||
CanvasToolToolbarBinding { "btn-mask-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_line, true, false },
|
||||
CanvasToolToolbarBinding { "btn-bucket", CanvasToolToolbarAction::select_mode, CanvasToolMode::flood_fill, true, false },
|
||||
},
|
||||
CanvasToolMode::draw,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_toolbar_binding_action(
|
||||
const CanvasToolToolbarBinding& binding,
|
||||
bool current_mode_is_draw) noexcept
|
||||
{
|
||||
switch (binding.action) {
|
||||
case CanvasToolToolbarAction::select_mode:
|
||||
return plan_canvas_tool_select(binding.mode);
|
||||
case CanvasToolToolbarAction::toggle_picking:
|
||||
return plan_canvas_tool_pick_toggle(current_mode_is_draw);
|
||||
case CanvasToolToolbarAction::toggle_touch_lock:
|
||||
return plan_canvas_tool_touch_lock_toggle();
|
||||
}
|
||||
|
||||
return plan_canvas_tool_select(CanvasToolMode::draw);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state(
|
||||
CanvasToolMode mode,
|
||||
bool picking,
|
||||
bool touch_lock) noexcept
|
||||
{
|
||||
CanvasToolButtonState state;
|
||||
state.mode = mode;
|
||||
state.pick_active = mode == CanvasToolMode::draw && picking;
|
||||
state.touch_lock_active = touch_lock;
|
||||
state.pen_active = mode == CanvasToolMode::draw;
|
||||
state.erase_active = mode == CanvasToolMode::erase;
|
||||
state.line_active = mode == CanvasToolMode::line;
|
||||
state.camera_active = mode == CanvasToolMode::camera;
|
||||
state.grid_active = mode == CanvasToolMode::grid;
|
||||
state.copy_active = mode == CanvasToolMode::copy;
|
||||
state.cut_active = mode == CanvasToolMode::cut;
|
||||
state.fill_active = mode == CanvasToolMode::fill;
|
||||
state.mask_free_active = mode == CanvasToolMode::mask_free;
|
||||
state.mask_line_active = mode == CanvasToolMode::mask_line;
|
||||
state.flood_fill_active = mode == CanvasToolMode::flood_fill;
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool canvas_tool_mode_is_paint(CanvasToolMode mode) noexcept
|
||||
{
|
||||
return mode == CanvasToolMode::draw || mode == CanvasToolMode::erase;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasCursorVisibilityPlan> plan_canvas_cursor_visibility(
|
||||
const CanvasCursorVisibilityInput& input)
|
||||
{
|
||||
CanvasCursorVisibilityPlan plan;
|
||||
plan.paint_mode = canvas_tool_mode_is_paint(input.mode);
|
||||
if (!plan.paint_mode) {
|
||||
plan.visible = true;
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
switch (input.visibility_mode) {
|
||||
case CanvasCursorVisibilityMode::always:
|
||||
plan.visible = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::never:
|
||||
plan.visible = false;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::small_brush:
|
||||
if (!input.has_current_brush) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor small-brush mode requires a current brush"));
|
||||
}
|
||||
if (!std::isfinite(input.brush_tip_size) || input.brush_tip_size < 0.0F) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor brush size must be finite and non-negative"));
|
||||
}
|
||||
plan.visible = input.brush_tip_size < 10.0F;
|
||||
plan.uses_brush_size = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::not_painting:
|
||||
plan.visible = !input.pen_is_drawing;
|
||||
plan.uses_pen_state = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (input.alt_down || input.pen_is_resizing || input.pen_is_picking) {
|
||||
plan.visible = true;
|
||||
plan.forced_visible_by_modifier_or_tool = true;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
|
||||
const CanvasToolPlan& plan,
|
||||
CanvasToolServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case CanvasToolOperation::select_mode:
|
||||
if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) {
|
||||
return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode");
|
||||
}
|
||||
if (plan.transform_action != transform_action_for_mode(plan.mode)) {
|
||||
return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action");
|
||||
}
|
||||
services.select_toolbar_button(plan.mode);
|
||||
if (plan.transform_action != CanvasToolTransformAction::none) {
|
||||
services.set_transform_action(plan.transform_action);
|
||||
}
|
||||
services.set_canvas_mode(plan.mode);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case CanvasToolOperation::toggle_picking:
|
||||
if (!plan.requires_draw_mode) {
|
||||
return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode");
|
||||
}
|
||||
if (plan.no_op) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
if (!plan.toggles_picking) {
|
||||
return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op");
|
||||
}
|
||||
services.toggle_picking();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case CanvasToolOperation::toggle_touch_lock:
|
||||
if (!plan.toggles_touch_lock || plan.no_op) {
|
||||
return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock");
|
||||
}
|
||||
services.toggle_touch_lock();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown canvas tool operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
115
src/app_core/canvas_view.h
Normal file
115
src/app_core/canvas_view.h
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasViewCursorMode {
|
||||
never = 0,
|
||||
small_brush = 1,
|
||||
not_painting = 2,
|
||||
always = 3,
|
||||
};
|
||||
|
||||
struct CanvasCameraState {
|
||||
std::array<float, 16> rotation {};
|
||||
std::array<float, 3> position {};
|
||||
float field_of_view_degrees = 85.0F;
|
||||
std::array<float, 2> pan {};
|
||||
};
|
||||
|
||||
struct CanvasViewDensityPlan {
|
||||
float density = 1.0F;
|
||||
bool recreates_buffers = true;
|
||||
};
|
||||
|
||||
struct CanvasViewCursorModePlan {
|
||||
CanvasViewCursorMode mode = CanvasViewCursorMode::never;
|
||||
};
|
||||
|
||||
class CanvasViewServices {
|
||||
public:
|
||||
virtual ~CanvasViewServices() = default;
|
||||
|
||||
virtual void reset_camera(const CanvasCameraState& state) = 0;
|
||||
virtual void set_density(const CanvasViewDensityPlan& plan) = 0;
|
||||
virtual void set_cursor_mode(const CanvasViewCursorModePlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr CanvasCameraState plan_canvas_camera_reset() noexcept
|
||||
{
|
||||
CanvasCameraState state;
|
||||
state.rotation = {
|
||||
1.0F, 0.0F, 0.0F, 0.0F,
|
||||
0.0F, 1.0F, 0.0F, 0.0F,
|
||||
0.0F, 0.0F, 1.0F, 0.0F,
|
||||
0.0F, 0.0F, 0.0F, 1.0F,
|
||||
};
|
||||
state.position = { 0.0F, 0.0F, 0.0F };
|
||||
state.field_of_view_degrees = 85.0F;
|
||||
state.pan = { 0.0F, 0.0F };
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasViewDensityPlan> plan_canvas_view_density(float density)
|
||||
{
|
||||
if (!std::isfinite(density) || density <= 0.0F) {
|
||||
return pp::foundation::Result<CanvasViewDensityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas view density must be finite and positive"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasViewDensityPlan>::success(CanvasViewDensityPlan {
|
||||
.density = density,
|
||||
.recreates_buffers = true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasViewCursorModePlan> plan_canvas_view_cursor_mode(int mode)
|
||||
{
|
||||
if (mode < static_cast<int>(CanvasViewCursorMode::never)
|
||||
|| mode > static_cast<int>(CanvasViewCursorMode::always)) {
|
||||
return pp::foundation::Result<CanvasViewCursorModePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("canvas cursor mode is out of range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasViewCursorModePlan>::success(CanvasViewCursorModePlan {
|
||||
.mode = static_cast<CanvasViewCursorMode>(mode),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_camera_reset(CanvasViewServices& services)
|
||||
{
|
||||
services.reset_camera(plan_canvas_camera_reset());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_view_density(
|
||||
float density,
|
||||
CanvasViewServices& services)
|
||||
{
|
||||
const auto plan = plan_canvas_view_density(density);
|
||||
if (!plan) {
|
||||
return plan.status();
|
||||
}
|
||||
|
||||
services.set_density(plan.value());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_view_cursor_mode(
|
||||
int mode,
|
||||
CanvasViewServices& services)
|
||||
{
|
||||
const auto plan = plan_canvas_view_cursor_mode(mode);
|
||||
if (!plan) {
|
||||
return plan.status();
|
||||
}
|
||||
|
||||
services.set_cursor_mode(plan.value());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
97
src/app_core/command_convert.h
Normal file
97
src/app_core/command_convert.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CommandConvertStep {
|
||||
apply_renderer_state,
|
||||
create_canvas,
|
||||
open_project,
|
||||
export_equirectangular,
|
||||
};
|
||||
|
||||
struct CommandConvertPlan {
|
||||
std::string project_path;
|
||||
std::string output_path;
|
||||
int canvas_resolution = 0;
|
||||
std::vector<CommandConvertStep> steps;
|
||||
};
|
||||
|
||||
class CommandConvertServices {
|
||||
public:
|
||||
virtual ~CommandConvertServices() = default;
|
||||
|
||||
virtual void apply_renderer_state() = 0;
|
||||
virtual void create_canvas(int canvas_resolution) = 0;
|
||||
virtual void open_project(std::string_view project_path) = 0;
|
||||
virtual void export_equirectangular(std::string_view output_path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CommandConvertPlan> plan_command_convert(
|
||||
std::string_view project_path,
|
||||
std::string_view output_path,
|
||||
int canvas_resolution)
|
||||
{
|
||||
if (project_path.empty()) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert project path must not be empty"));
|
||||
}
|
||||
|
||||
if (output_path.empty()) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert output path must not be empty"));
|
||||
}
|
||||
|
||||
if (canvas_resolution < 1) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert canvas resolution must be positive"));
|
||||
}
|
||||
|
||||
CommandConvertPlan plan;
|
||||
plan.project_path = std::string(project_path);
|
||||
plan.output_path = std::string(output_path);
|
||||
plan.canvas_resolution = canvas_resolution;
|
||||
plan.steps = {
|
||||
CommandConvertStep::apply_renderer_state,
|
||||
CommandConvertStep::create_canvas,
|
||||
CommandConvertStep::open_project,
|
||||
CommandConvertStep::export_equirectangular,
|
||||
};
|
||||
return pp::foundation::Result<CommandConvertPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_command_convert_plan(
|
||||
const CommandConvertPlan& plan,
|
||||
CommandConvertServices& services)
|
||||
{
|
||||
if (plan.project_path.empty() || plan.output_path.empty() || plan.canvas_resolution < 1) {
|
||||
return pp::foundation::Status::invalid_argument("convert plan is malformed");
|
||||
}
|
||||
|
||||
for (const auto step : plan.steps) {
|
||||
switch (step) {
|
||||
case CommandConvertStep::apply_renderer_state:
|
||||
services.apply_renderer_state();
|
||||
break;
|
||||
case CommandConvertStep::create_canvas:
|
||||
services.create_canvas(plan.canvas_resolution);
|
||||
break;
|
||||
case CommandConvertStep::open_project:
|
||||
services.open_project(plan.project_path);
|
||||
break;
|
||||
case CommandConvertStep::export_equirectangular:
|
||||
services.export_equirectangular(plan.output_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
836
src/app_core/document_animation.h
Normal file
836
src/app_core/document_animation.h
Normal file
@@ -0,0 +1,836 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr int document_animation_default_frame_duration = 1;
|
||||
|
||||
enum class DocumentAnimationOperation {
|
||||
add_frame,
|
||||
duplicate_frame,
|
||||
remove_frame,
|
||||
adjust_duration,
|
||||
move_frame,
|
||||
select_frame,
|
||||
goto_frame,
|
||||
goto_next,
|
||||
goto_previous,
|
||||
playback_step,
|
||||
toggle_playback,
|
||||
set_onion_size,
|
||||
};
|
||||
|
||||
enum class DocumentAnimationPanelAction {
|
||||
goto_frame,
|
||||
next_frame,
|
||||
previous_frame,
|
||||
playback_step,
|
||||
toggle_playback,
|
||||
};
|
||||
|
||||
struct DocumentAnimationPanelState {
|
||||
int total_duration = 1;
|
||||
int current_frame = 0;
|
||||
bool playback_active = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationOperationPlan {
|
||||
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int selected_frame = 0;
|
||||
int target_frame = 0;
|
||||
int frame_duration = document_animation_default_frame_duration;
|
||||
int duration_delta = 0;
|
||||
int move_offset = 0;
|
||||
int onion_size = 1;
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
int playback_idle_ms = 100;
|
||||
bool requires_selected_frame = false;
|
||||
bool mutates_document = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_canvas_animation = false;
|
||||
bool marks_unsaved = false;
|
||||
bool playback_was_active = false;
|
||||
bool playback_active = false;
|
||||
bool resets_playback_timer = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationOnionFrameRange {
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int onion_size = 0;
|
||||
int first_frame = 0;
|
||||
int last_frame = 0;
|
||||
};
|
||||
|
||||
inline constexpr float document_animation_timeline_frame_width = 35.0F;
|
||||
|
||||
struct DocumentAnimationTimelineScrubPlan {
|
||||
int total_duration = 1;
|
||||
float cursor_x = 0.0F;
|
||||
float frame_width = document_animation_timeline_frame_width;
|
||||
int target_frame = 0;
|
||||
};
|
||||
|
||||
struct DocumentAnimationLayerInput {
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
std::vector<int> frame_durations;
|
||||
};
|
||||
|
||||
struct DocumentAnimationFrameView {
|
||||
int frame_index = 0;
|
||||
int duration = document_animation_default_frame_duration;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationLayerView {
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
bool current = false;
|
||||
std::vector<DocumentAnimationFrameView> frames;
|
||||
};
|
||||
|
||||
struct DocumentAnimationPanelView {
|
||||
int total_duration = 1;
|
||||
int current_frame = 0;
|
||||
int onion_size = 0;
|
||||
std::uint32_t selected_layer_id = 0;
|
||||
int selected_frame = -1;
|
||||
bool has_selected_frame = false;
|
||||
std::vector<DocumentAnimationLayerView> layers;
|
||||
};
|
||||
|
||||
class DocumentAnimationServices {
|
||||
public:
|
||||
virtual ~DocumentAnimationServices() = default;
|
||||
|
||||
virtual void add_frame() = 0;
|
||||
virtual void duplicate_frame(int selected_frame) = 0;
|
||||
virtual void remove_frame(int selected_frame, int target_frame) = 0;
|
||||
virtual void set_frame_duration(int selected_frame, int duration) = 0;
|
||||
virtual int move_frame(int selected_frame, int move_offset) = 0;
|
||||
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
|
||||
virtual void select_layer(int layer_index) = 0;
|
||||
virtual void goto_frame(int target_frame) = 0;
|
||||
virtual void set_timeline_frame(int target_frame) = 0;
|
||||
virtual void set_onion_size(int onion_size) = 0;
|
||||
virtual void capture_playback_restore_mode() = 0;
|
||||
virtual void enter_playback_camera_mode() = 0;
|
||||
virtual void restore_playback_canvas_mode() = 0;
|
||||
virtual void set_playback_active(bool active) = 0;
|
||||
virtual void reset_playback_timer() = 0;
|
||||
virtual void set_playback_idle_ms(int idle_ms) = 0;
|
||||
virtual void update_canvas_animation() = 0;
|
||||
virtual void update_frame_status() = 0;
|
||||
virtual void reload_animation_layers() = 0;
|
||||
virtual void mark_unsaved() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
|
||||
{
|
||||
if (frame_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
|
||||
int frame_count,
|
||||
int index) noexcept
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= frame_count) {
|
||||
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
|
||||
{
|
||||
if (duration < 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationPanelView> plan_animation_panel_view(
|
||||
const std::vector<DocumentAnimationLayerInput>& layers,
|
||||
int total_duration,
|
||||
int current_layer_index,
|
||||
int current_frame,
|
||||
std::uint32_t selected_layer_id,
|
||||
int selected_frame,
|
||||
int onion_size)
|
||||
{
|
||||
if (layers.empty()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation panel requires at least one layer"));
|
||||
}
|
||||
|
||||
const auto timeline_status = validate_animation_frame_index(total_duration, current_frame);
|
||||
if (!timeline_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(timeline_status);
|
||||
}
|
||||
|
||||
if (current_layer_index < 0 || current_layer_index >= static_cast<int>(layers.size())) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation layer index is outside the document"));
|
||||
}
|
||||
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationPanelView view;
|
||||
view.total_duration = total_duration;
|
||||
view.current_frame = current_frame;
|
||||
view.onion_size = onion_size;
|
||||
view.selected_layer_id = selected_layer_id;
|
||||
view.selected_frame = selected_frame;
|
||||
view.layers.reserve(layers.size());
|
||||
|
||||
for (std::size_t i = 0; i < layers.size(); ++i) {
|
||||
const auto& input = layers[i];
|
||||
if (input.layer_index < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||
}
|
||||
if (input.frame_durations.empty()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must contain at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationLayerView layer;
|
||||
layer.layer_index = input.layer_index;
|
||||
layer.layer_id = input.layer_id;
|
||||
layer.name = input.name;
|
||||
layer.visible = input.visible;
|
||||
layer.current = input.layer_index == current_layer_index;
|
||||
layer.frames.reserve(input.frame_durations.size());
|
||||
|
||||
for (std::size_t frame_index = 0; frame_index < input.frame_durations.size(); ++frame_index) {
|
||||
const int duration = input.frame_durations[frame_index];
|
||||
const auto duration_status = validate_animation_frame_duration(duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(duration_status);
|
||||
}
|
||||
|
||||
const bool selected = selected_frame >= 0
|
||||
&& input.layer_id == selected_layer_id
|
||||
&& static_cast<int>(frame_index) == selected_frame;
|
||||
view.has_selected_frame = view.has_selected_frame || selected;
|
||||
layer.frames.push_back(DocumentAnimationFrameView {
|
||||
.frame_index = static_cast<int>(frame_index),
|
||||
.duration = duration,
|
||||
.selected = selected,
|
||||
});
|
||||
}
|
||||
|
||||
view.layers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::success(std::move(view));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
|
||||
int frame_count,
|
||||
int current_frame,
|
||||
int onion_size)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, current_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(index_status);
|
||||
}
|
||||
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
const auto first = std::max<std::int64_t>(
|
||||
static_cast<std::int64_t>(current_frame) - static_cast<std::int64_t>(onion_size),
|
||||
0);
|
||||
const auto last = std::min<std::int64_t>(
|
||||
static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(onion_size),
|
||||
static_cast<std::int64_t>(frame_count) - 1);
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::success(
|
||||
DocumentAnimationOnionFrameRange {
|
||||
.frame_count = frame_count,
|
||||
.current_frame = current_frame,
|
||||
.onion_size = onion_size,
|
||||
.first_frame = static_cast<int>(first),
|
||||
.last_frame = static_cast<int>(last),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationTimelineScrubPlan> plan_animation_timeline_scrub(
|
||||
int total_duration,
|
||||
float cursor_x,
|
||||
float frame_width = document_animation_timeline_frame_width)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (!std::isfinite(cursor_x)) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline cursor position must be finite"));
|
||||
}
|
||||
|
||||
if (!std::isfinite(frame_width) || frame_width <= 0.0F) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline frame width must be positive and finite"));
|
||||
}
|
||||
|
||||
const auto raw_frame = static_cast<std::int64_t>(std::floor(cursor_x / frame_width));
|
||||
const auto target_frame = std::clamp<std::int64_t>(raw_frame, 0, total_duration - 1);
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::success(
|
||||
DocumentAnimationTimelineScrubPlan {
|
||||
.total_duration = total_duration,
|
||||
.cursor_x = cursor_x,
|
||||
.frame_width = frame_width,
|
||||
.target_frame = static_cast<int>(target_frame),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline float animation_onion_frame_alpha(
|
||||
const DocumentAnimationOnionFrameRange& range,
|
||||
int frame) noexcept
|
||||
{
|
||||
if (frame < range.first_frame || frame > range.last_frame) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const int distance = frame >= range.current_frame
|
||||
? frame - range.current_frame
|
||||
: range.current_frame - frame;
|
||||
if (distance > range.onion_size) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 1.0f - static_cast<float>(distance) / static_cast<float>(range.onion_size + 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
|
||||
int frame_count,
|
||||
int current_frame)
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
if (current_frame < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::add_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.current_frame = current_frame;
|
||||
plan.selected_frame = frame_count;
|
||||
plan.target_frame = current_frame;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::duplicate_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame + 1;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (frame_count <= 1) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::remove_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = std::min(selected_frame, frame_count - 2);
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int current_duration,
|
||||
int delta)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto duration_status = validate_animation_frame_duration(current_duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
|
||||
}
|
||||
|
||||
if (delta == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
|
||||
}
|
||||
|
||||
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::adjust_duration;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.frame_duration = std::max(current_duration + delta, 1);
|
||||
plan.duration_delta = delta;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.frame_duration != current_duration;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_canvas_animation = plan.mutates_document;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::move_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
|
||||
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
|
||||
plan.move_offset = offset;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.target_frame != selected_frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
|
||||
int frame_count,
|
||||
int layer_index,
|
||||
std::uint32_t layer_id,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (layer_index < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::select_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.layer_index = layer_index;
|
||||
plan.layer_id = layer_id;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
|
||||
int total_duration,
|
||||
int frame)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (frame < 0 || frame >= total_duration) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::goto_frame;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = frame;
|
||||
plan.target_frame = frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
|
||||
if (!current_status) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = current_frame;
|
||||
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
|
||||
% static_cast<std::int64_t>(total_duration);
|
||||
if (target < 0) {
|
||||
target += total_duration;
|
||||
}
|
||||
plan.target_frame = static_cast<int>(target);
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
|
||||
if (!step) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
|
||||
}
|
||||
|
||||
auto plan = step.value();
|
||||
plan.operation = DocumentAnimationOperation::playback_step;
|
||||
plan.move_offset = offset;
|
||||
plan.reloads_animation_layers = false;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
|
||||
bool playback_active)
|
||||
{
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::toggle_playback;
|
||||
plan.playback_was_active = playback_active;
|
||||
plan.playback_active = !playback_active;
|
||||
plan.playback_idle_ms = playback_active ? 100 : 10;
|
||||
plan.resets_playback_timer = !playback_active;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
|
||||
{
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::set_onion_size;
|
||||
plan.onion_size = onion_size;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
|
||||
DocumentAnimationPanelAction action,
|
||||
const DocumentAnimationPanelState& state,
|
||||
int target_frame = 0)
|
||||
{
|
||||
switch (action) {
|
||||
case DocumentAnimationPanelAction::goto_frame:
|
||||
return plan_animation_goto_frame(state.total_duration, target_frame);
|
||||
|
||||
case DocumentAnimationPanelAction::next_frame:
|
||||
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
|
||||
|
||||
case DocumentAnimationPanelAction::previous_frame:
|
||||
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
|
||||
|
||||
case DocumentAnimationPanelAction::playback_step:
|
||||
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
|
||||
|
||||
case DocumentAnimationPanelAction::toggle_playback:
|
||||
return plan_animation_playback_toggle(state.playback_active);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown animation panel action"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
|
||||
const DocumentAnimationOperationPlan& plan) noexcept
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentAnimationOperation::add_frame:
|
||||
if (!plan.mutates_document || !plan.marks_unsaved) {
|
||||
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
|
||||
}
|
||||
return validate_animation_frame_count(plan.frame_count);
|
||||
|
||||
case DocumentAnimationOperation::duplicate_frame:
|
||||
case DocumentAnimationOperation::remove_frame:
|
||||
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
|
||||
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
|
||||
}
|
||||
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
|
||||
case DocumentAnimationOperation::adjust_duration:
|
||||
if (!plan.requires_selected_frame) {
|
||||
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
|
||||
}
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
}
|
||||
return validate_animation_frame_duration(plan.frame_duration);
|
||||
|
||||
case DocumentAnimationOperation::move_frame:
|
||||
if (!plan.requires_selected_frame || plan.move_offset == 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
|
||||
case DocumentAnimationOperation::select_frame:
|
||||
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
|
||||
}
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||
|
||||
case DocumentAnimationOperation::goto_frame:
|
||||
case DocumentAnimationOperation::goto_next:
|
||||
case DocumentAnimationOperation::goto_previous:
|
||||
case DocumentAnimationOperation::playback_step:
|
||||
if (!plan.updates_canvas_animation) {
|
||||
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
|
||||
}
|
||||
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||
|
||||
case DocumentAnimationOperation::toggle_playback:
|
||||
if (plan.playback_active == plan.playback_was_active) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
|
||||
}
|
||||
if (plan.playback_idle_ms <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
|
||||
}
|
||||
if (plan.playback_active && !plan.resets_playback_timer) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::set_onion_size:
|
||||
if (plan.onion_size < 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
|
||||
}
|
||||
if (!plan.updates_canvas_animation) {
|
||||
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
|
||||
const DocumentAnimationOperationPlan& plan,
|
||||
DocumentAnimationServices& services)
|
||||
{
|
||||
const auto validation = validate_animation_operation_plan(plan);
|
||||
if (!validation.ok()) {
|
||||
return validation;
|
||||
}
|
||||
|
||||
switch (plan.operation) {
|
||||
case DocumentAnimationOperation::add_frame:
|
||||
services.add_frame();
|
||||
services.mark_unsaved();
|
||||
services.update_canvas_animation();
|
||||
services.reload_animation_layers();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::duplicate_frame:
|
||||
services.duplicate_frame(plan.selected_frame);
|
||||
services.mark_unsaved();
|
||||
services.update_canvas_animation();
|
||||
services.reload_animation_layers();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::remove_frame:
|
||||
services.remove_frame(plan.selected_frame, plan.target_frame);
|
||||
services.mark_unsaved();
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(plan.target_frame);
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::adjust_duration:
|
||||
if (plan.mutates_document) {
|
||||
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
|
||||
services.mark_unsaved();
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.update_canvas_animation();
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::move_frame:
|
||||
{
|
||||
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
|
||||
if (plan.marks_unsaved) {
|
||||
services.mark_unsaved();
|
||||
}
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(actual_target_frame);
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
case DocumentAnimationOperation::goto_frame:
|
||||
case DocumentAnimationOperation::goto_next:
|
||||
case DocumentAnimationOperation::goto_previous:
|
||||
services.goto_frame(plan.target_frame);
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::select_frame:
|
||||
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(plan.target_frame);
|
||||
}
|
||||
services.select_layer(plan.layer_index);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::playback_step:
|
||||
services.goto_frame(plan.target_frame);
|
||||
services.set_timeline_frame(plan.target_frame);
|
||||
services.update_frame_status();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::toggle_playback:
|
||||
if (plan.playback_active) {
|
||||
services.capture_playback_restore_mode();
|
||||
services.enter_playback_camera_mode();
|
||||
if (plan.resets_playback_timer) {
|
||||
services.reset_playback_timer();
|
||||
}
|
||||
} else {
|
||||
services.restore_playback_canvas_mode();
|
||||
}
|
||||
services.set_playback_active(plan.playback_active);
|
||||
services.set_playback_idle_ms(plan.playback_idle_ms);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::set_onion_size:
|
||||
services.set_onion_size(plan.onion_size);
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.update_canvas_animation();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
526
src/app_core/document_canvas.h
Normal file
526
src/app_core/document_canvas.h
Normal file
@@ -0,0 +1,526 @@
|
||||
#pragma once
|
||||
|
||||
#include "document/document.h"
|
||||
#include "document/ppi_export.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct DocumentCanvasClearPlan {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 0.0F;
|
||||
bool clears_canvas = false;
|
||||
bool records_undo = false;
|
||||
bool marks_unsaved = false;
|
||||
bool no_op = true;
|
||||
};
|
||||
|
||||
struct DocumentCanvasFacePayloadInput {
|
||||
std::uint32_t frame_index = 0;
|
||||
std::uint32_t face_index = 0;
|
||||
std::uint32_t x = 0;
|
||||
std::uint32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::span<const std::uint8_t> rgba8;
|
||||
};
|
||||
|
||||
struct DocumentCanvasLayerSnapshotInput {
|
||||
std::string_view name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
int blend_mode = 0;
|
||||
std::span<const std::uint32_t> frame_durations_ms;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
std::span<const DocumentCanvasFacePayloadInput> captured_face_payloads;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSnapshotInput {
|
||||
bool has_canvas = true;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::size_t active_layer_index = 0;
|
||||
std::size_t active_frame_index = 0;
|
||||
std::span<const DocumentCanvasLayerSnapshotInput> layers;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSnapshotResult {
|
||||
pp::document::CanvasDocument document;
|
||||
std::size_t layer_count = 0;
|
||||
std::size_t frame_count = 0;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
std::size_t captured_face_payloads = 0;
|
||||
bool metadata_only = false;
|
||||
bool requires_renderer_payload_readback = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSaveSnapshotReport {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::size_t layer_count = 0;
|
||||
std::size_t frame_count = 0;
|
||||
std::size_t captured_face_payloads = 0;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
bool payload_complete = false;
|
||||
bool can_export_ppi = false;
|
||||
};
|
||||
|
||||
enum class DocumentCanvasSaveWriterAction {
|
||||
use_document_ppi_writer,
|
||||
use_legacy_project_save,
|
||||
};
|
||||
|
||||
struct DocumentCanvasSaveWriterRoutePlan {
|
||||
DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save;
|
||||
bool payload_complete = false;
|
||||
bool can_export_ppi = false;
|
||||
bool uses_document_ppi_writer = false;
|
||||
std::string_view fallback_reason;
|
||||
};
|
||||
|
||||
struct DocumentCanvasPpiExportResult {
|
||||
DocumentCanvasSaveSnapshotReport report;
|
||||
std::vector<std::byte> bytes;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveTargetPlan {
|
||||
std::string target_path;
|
||||
std::string file_name;
|
||||
std::string temporary_path;
|
||||
std::string timelapse_path;
|
||||
};
|
||||
|
||||
enum class DocumentCanvasProjectSaveWriteAction {
|
||||
write_direct_to_target,
|
||||
write_temporary_then_swap,
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveWritePlan {
|
||||
DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target;
|
||||
std::string write_path;
|
||||
std::string target_path;
|
||||
std::string temporary_path;
|
||||
bool target_exists = false;
|
||||
bool uses_temporary = false;
|
||||
bool falls_back_to_direct_on_temporary_open_failure = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveCommitInput {
|
||||
bool used_temporary = false;
|
||||
bool target_remove_attempted = false;
|
||||
bool target_remove_succeeded = false;
|
||||
bool temporary_rename_attempted = false;
|
||||
bool temporary_rename_succeeded = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveCommitPlan {
|
||||
bool saved = false;
|
||||
bool used_temporary = false;
|
||||
bool target_removed = false;
|
||||
bool temporary_renamed = false;
|
||||
bool target_may_be_missing = false;
|
||||
std::string_view log_message;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSavePostCommitInput {
|
||||
bool save_succeeded = false;
|
||||
bool timelapse_encoder_available = false;
|
||||
bool progress_ui_visible = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSavePostCommitPlan {
|
||||
bool marks_document_clean = false;
|
||||
bool marks_new_document_committed = false;
|
||||
bool saves_timelapse_sidecar = false;
|
||||
bool flushes_platform_storage = false;
|
||||
bool dismisses_progress_ui = false;
|
||||
bool updates_title = true;
|
||||
};
|
||||
|
||||
class DocumentCanvasClearServices {
|
||||
public:
|
||||
virtual ~DocumentCanvasClearServices() = default;
|
||||
|
||||
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
|
||||
}
|
||||
if (value < 0.0F || value > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
|
||||
DocumentCanvasSnapshotInput input)
|
||||
{
|
||||
if (!input.has_canvas) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
|
||||
}
|
||||
|
||||
if (input.layers.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
|
||||
}
|
||||
|
||||
std::size_t frame_count = 1U;
|
||||
std::size_t pending_face_payloads = 0U;
|
||||
std::size_t captured_face_payloads = 0U;
|
||||
for (const auto& layer : input.layers) {
|
||||
frame_count = std::max(frame_count, layer.frame_durations_ms.size());
|
||||
pending_face_payloads += layer.pending_face_payloads;
|
||||
captured_face_payloads += layer.captured_face_payloads.size();
|
||||
}
|
||||
|
||||
if (input.active_layer_index >= input.layers.size()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
|
||||
}
|
||||
|
||||
if (input.active_frame_index >= frame_count) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
|
||||
}
|
||||
|
||||
std::vector<pp::document::AnimationFrame> root_frames;
|
||||
root_frames.reserve(frame_count);
|
||||
for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
||||
std::uint32_t duration_ms = 100U;
|
||||
for (const auto& layer : input.layers) {
|
||||
if (frame_index < layer.frame_durations_ms.size()) {
|
||||
duration_ms = layer.frame_durations_ms[frame_index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||
}
|
||||
|
||||
std::vector<std::string> layer_names;
|
||||
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
|
||||
std::vector<pp::document::DocumentLayerConfig> layer_configs;
|
||||
layer_names.reserve(input.layers.size());
|
||||
layer_frames.reserve(input.layers.size());
|
||||
layer_configs.reserve(input.layers.size());
|
||||
|
||||
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
||||
const auto& layer = input.layers[layer_index];
|
||||
if (layer.name.empty()) {
|
||||
layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
|
||||
} else {
|
||||
layer_names.push_back(std::string(layer.name));
|
||||
}
|
||||
|
||||
layer_frames.push_back({});
|
||||
auto& frames = layer_frames.back();
|
||||
frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
|
||||
if (layer.frame_durations_ms.empty()) {
|
||||
frames = root_frames;
|
||||
} else {
|
||||
for (const auto duration_ms : layer.frame_durations_ms) {
|
||||
frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||
}
|
||||
}
|
||||
|
||||
layer_configs.push_back(pp::document::DocumentLayerConfig {
|
||||
.name = layer_names.back(),
|
||||
.visible = layer.visible,
|
||||
.alpha_locked = layer.alpha_locked,
|
||||
.opacity = layer.opacity,
|
||||
.blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
|
||||
.frames = std::span<const pp::document::AnimationFrame>(frames),
|
||||
});
|
||||
}
|
||||
|
||||
auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
|
||||
.width = input.width,
|
||||
.height = input.height,
|
||||
.layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
|
||||
.frames = std::span<const pp::document::AnimationFrame>(root_frames),
|
||||
});
|
||||
if (!document) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
|
||||
}
|
||||
|
||||
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
||||
for (const auto& payload : input.layers[layer_index].captured_face_payloads) {
|
||||
pp::document::LayerFacePixels pixels {
|
||||
.face_index = payload.face_index,
|
||||
.x = payload.x,
|
||||
.y = payload.y,
|
||||
.width = payload.width,
|
||||
.height = payload.height,
|
||||
.rgba8 = std::vector<std::uint8_t>(payload.rgba8.begin(), payload.rgba8.end()),
|
||||
};
|
||||
const auto payload_status = document.value().set_layer_frame_face_pixels(
|
||||
layer_index,
|
||||
payload.frame_index,
|
||||
std::move(pixels));
|
||||
if (!payload_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(payload_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto active_status = document.value().set_active_layer(input.active_layer_index);
|
||||
if (!active_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||
}
|
||||
|
||||
active_status = document.value().set_active_frame(input.active_frame_index);
|
||||
if (!active_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
|
||||
.document = std::move(document.value()),
|
||||
.layer_count = input.layers.size(),
|
||||
.frame_count = frame_count,
|
||||
.pending_face_payloads = pending_face_payloads,
|
||||
.captured_face_payloads = captured_face_payloads,
|
||||
.metadata_only = captured_face_payloads == 0U,
|
||||
.requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report(
|
||||
const DocumentCanvasSnapshotResult& snapshot) noexcept
|
||||
{
|
||||
return DocumentCanvasSaveSnapshotReport {
|
||||
.width = snapshot.document.width(),
|
||||
.height = snapshot.document.height(),
|
||||
.layer_count = snapshot.layer_count,
|
||||
.frame_count = snapshot.frame_count,
|
||||
.captured_face_payloads = snapshot.captured_face_payloads,
|
||||
.pending_face_payloads = snapshot.pending_face_payloads,
|
||||
.payload_complete = !snapshot.requires_renderer_payload_readback,
|
||||
.can_export_ppi = !snapshot.requires_renderer_payload_readback,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route(
|
||||
DocumentCanvasSaveSnapshotReport report) noexcept
|
||||
{
|
||||
DocumentCanvasSaveWriterRoutePlan plan;
|
||||
plan.payload_complete = report.payload_complete;
|
||||
plan.can_export_ppi = report.can_export_ppi;
|
||||
|
||||
if (!report.payload_complete || !report.can_export_ppi) {
|
||||
plan.fallback_reason = "canvas document snapshot still requires renderer payload readback";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer;
|
||||
plan.uses_document_ppi_writer = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasPpiExportResult>
|
||||
export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot)
|
||||
{
|
||||
const auto report = make_document_canvas_save_snapshot_report(snapshot);
|
||||
const auto route = plan_document_canvas_save_writer_route(report);
|
||||
if (!route.uses_document_ppi_writer) {
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(
|
||||
pp::foundation::Status::invalid_argument(route.fallback_reason.data()));
|
||||
}
|
||||
|
||||
auto bytes = pp::document::export_ppi_project_document(snapshot.document);
|
||||
if (!bytes) {
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(bytes.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::success(DocumentCanvasPpiExportResult {
|
||||
.report = report,
|
||||
.bytes = std::move(bytes.value()),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>
|
||||
plan_document_canvas_project_save_target(
|
||||
std::string_view data_directory,
|
||||
std::string_view target_path)
|
||||
{
|
||||
if (data_directory.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save data directory must not be empty"));
|
||||
}
|
||||
if (target_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save target path must not be empty"));
|
||||
}
|
||||
|
||||
const auto basename_start = target_path.find_last_of("/\\");
|
||||
const auto file_name_start = basename_start == std::string_view::npos ? 0U : basename_start + 1U;
|
||||
auto file_name = target_path.substr(file_name_start);
|
||||
if (file_name.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save target file name must not be empty"));
|
||||
}
|
||||
|
||||
constexpr std::string_view ppi_extension = ".ppi";
|
||||
if (file_name.size() > ppi_extension.size()
|
||||
&& file_name.substr(file_name.size() - ppi_extension.size()) == ppi_extension) {
|
||||
file_name.remove_suffix(ppi_extension.size());
|
||||
}
|
||||
|
||||
DocumentCanvasProjectSaveTargetPlan plan;
|
||||
plan.target_path = std::string(target_path);
|
||||
plan.file_name = std::string(file_name);
|
||||
plan.temporary_path.reserve(data_directory.size() + plan.file_name.size() + 10U);
|
||||
plan.temporary_path += data_directory;
|
||||
plan.temporary_path += "/";
|
||||
plan.temporary_path += plan.file_name;
|
||||
plan.temporary_path += ".tmp.ppi";
|
||||
plan.timelapse_path.reserve(data_directory.size() + plan.file_name.size() + 6U);
|
||||
plan.timelapse_path += data_directory;
|
||||
plan.timelapse_path += "/";
|
||||
plan.timelapse_path += plan.file_name;
|
||||
plan.timelapse_path += ".pptl";
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>
|
||||
plan_document_canvas_project_save_write(
|
||||
const DocumentCanvasProjectSaveTargetPlan& target,
|
||||
bool target_exists)
|
||||
{
|
||||
if (target.target_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save write target path must not be empty"));
|
||||
}
|
||||
|
||||
DocumentCanvasProjectSaveWritePlan plan;
|
||||
plan.target_exists = target_exists;
|
||||
plan.target_path = target.target_path;
|
||||
plan.temporary_path = target.temporary_path;
|
||||
|
||||
if (!target_exists) {
|
||||
plan.write_path = target.target_path;
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
if (target.temporary_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save temporary path must not be empty"));
|
||||
}
|
||||
|
||||
plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap;
|
||||
plan.write_path = target.temporary_path;
|
||||
plan.uses_temporary = true;
|
||||
plan.falls_back_to_direct_on_temporary_open_failure = true;
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasProjectSaveCommitPlan plan_document_canvas_project_save_commit(
|
||||
DocumentCanvasProjectSaveCommitInput input) noexcept
|
||||
{
|
||||
DocumentCanvasProjectSaveCommitPlan plan;
|
||||
plan.used_temporary = input.used_temporary;
|
||||
|
||||
if (!input.used_temporary) {
|
||||
plan.saved = true;
|
||||
plan.log_message = "project saved to target";
|
||||
return plan;
|
||||
}
|
||||
|
||||
if (!input.target_remove_attempted || !input.target_remove_succeeded) {
|
||||
plan.log_message = "could not remove target project before temporary swap";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.target_removed = true;
|
||||
if (!input.temporary_rename_attempted || !input.temporary_rename_succeeded) {
|
||||
plan.target_may_be_missing = true;
|
||||
plan.log_message = "temporary project not swapped after original removal";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.saved = true;
|
||||
plan.temporary_renamed = true;
|
||||
plan.log_message = "temporary project swapped successfully";
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasProjectSavePostCommitPlan plan_document_canvas_project_save_post_commit(
|
||||
DocumentCanvasProjectSavePostCommitInput input) noexcept
|
||||
{
|
||||
DocumentCanvasProjectSavePostCommitPlan plan;
|
||||
plan.dismisses_progress_ui = input.progress_ui_visible;
|
||||
|
||||
if (!input.save_succeeded) {
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.marks_document_clean = true;
|
||||
plan.marks_new_document_committed = true;
|
||||
plan.saves_timelapse_sidecar = input.timelapse_encoder_available;
|
||||
plan.flushes_platform_storage = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
||||
bool has_canvas,
|
||||
float r = 0.0F,
|
||||
float g = 0.0F,
|
||||
float b = 0.0F,
|
||||
float a = 0.0F) noexcept
|
||||
{
|
||||
const float channels[] { r, g, b, a };
|
||||
for (const float channel : channels) {
|
||||
const auto status = validate_clear_color_channel(channel);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
DocumentCanvasClearPlan plan;
|
||||
plan.r = r;
|
||||
plan.g = g;
|
||||
plan.b = b;
|
||||
plan.a = a;
|
||||
plan.clears_canvas = has_canvas;
|
||||
plan.records_undo = has_canvas;
|
||||
plan.marks_unsaved = has_canvas;
|
||||
plan.no_op = !has_canvas;
|
||||
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
|
||||
const DocumentCanvasClearPlan& plan,
|
||||
DocumentCanvasClearServices& services)
|
||||
{
|
||||
const float channels[] { plan.r, plan.g, plan.b, plan.a };
|
||||
for (const float channel : channels) {
|
||||
const auto status = validate_clear_color_channel(channel);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (plan.no_op || !plan.clears_canvas) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
315
src/app_core/document_cloud.h
Normal file
315
src/app_core/document_cloud.h
Normal file
@@ -0,0 +1,315 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CloudUploadAction {
|
||||
unavailable_no_canvas,
|
||||
show_save_required_warning,
|
||||
prompt_publish,
|
||||
};
|
||||
|
||||
enum class CloudBrowseAction {
|
||||
unavailable_no_canvas,
|
||||
show_browser,
|
||||
};
|
||||
|
||||
enum class CloudDownloadSelectionAction {
|
||||
wait_for_selection,
|
||||
start_download,
|
||||
};
|
||||
|
||||
enum class CloudTransferDirection {
|
||||
download,
|
||||
upload,
|
||||
};
|
||||
|
||||
enum class CloudTransferAction {
|
||||
reject_missing_source,
|
||||
reject_missing_destination,
|
||||
start_transfer,
|
||||
};
|
||||
|
||||
struct CloudUploadPlan {
|
||||
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
|
||||
bool save_before_upload = false;
|
||||
};
|
||||
|
||||
struct CloudBulkUploadPlan {
|
||||
std::size_t file_count = 0;
|
||||
int progress_total = 0;
|
||||
bool show_progress = false;
|
||||
};
|
||||
|
||||
struct CloudDownloadRequest {
|
||||
std::string selected_file;
|
||||
std::string selected_path;
|
||||
std::string selected_name;
|
||||
};
|
||||
|
||||
struct CloudTransferPlan {
|
||||
CloudTransferDirection direction = CloudTransferDirection::download;
|
||||
CloudTransferAction action = CloudTransferAction::reject_missing_source;
|
||||
bool enable_progress = false;
|
||||
bool disable_tls_verification = false;
|
||||
};
|
||||
|
||||
struct CloudTransferProgressPlan {
|
||||
bool notify = false;
|
||||
float fraction = 0.0F;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_save_required_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Warning",
|
||||
"This document needs to be saved before upload.",
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_publish_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Publish document",
|
||||
"Would you like to upload to the public domain?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_upload_success_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Success",
|
||||
"This document has been succesfully uploaded.",
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_upload_progress_dialog()
|
||||
{
|
||||
return plan_app_progress_dialog("Uploading", 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_bulk_upload_progress_dialog(int progress_total)
|
||||
{
|
||||
return plan_app_progress_dialog("Export Pano Image", progress_total);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_download_progress_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Downloading",
|
||||
"Download in progress",
|
||||
true);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string format_cloud_download_progress_message(float progress_fraction)
|
||||
{
|
||||
char buffer[64] {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"Download in progress %.2f%%",
|
||||
progress_fraction * 100.0F);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
class CloudServices {
|
||||
public:
|
||||
virtual ~CloudServices() = default;
|
||||
|
||||
virtual void show_save_required_warning() = 0;
|
||||
virtual void prompt_publish(bool save_before_upload) = 0;
|
||||
virtual void begin_bulk_upload(int progress_total, bool show_progress) = 0;
|
||||
virtual void upload_all_bulk_files() = 0;
|
||||
virtual void end_bulk_upload() = 0;
|
||||
virtual void show_browser() = 0;
|
||||
virtual void start_download(const CloudDownloadRequest& request) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
|
||||
bool has_canvas,
|
||||
bool is_new_document,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
if (!has_canvas) {
|
||||
return { CloudUploadAction::unavailable_no_canvas, false };
|
||||
}
|
||||
|
||||
if (is_new_document) {
|
||||
return { CloudUploadAction::show_save_required_warning, false };
|
||||
}
|
||||
|
||||
return { CloudUploadAction::prompt_publish, has_unsaved_changes };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudBrowseAction plan_cloud_browse(bool has_canvas) noexcept
|
||||
{
|
||||
return has_canvas
|
||||
? CloudBrowseAction::show_browser
|
||||
: CloudBrowseAction::unavailable_no_canvas;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudDownloadSelectionAction plan_cloud_download_selection(
|
||||
std::string_view selected_file) noexcept
|
||||
{
|
||||
return selected_file.empty()
|
||||
? CloudDownloadSelectionAction::wait_for_selection
|
||||
: CloudDownloadSelectionAction::start_download;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudBulkUploadPlan plan_cloud_bulk_upload(
|
||||
std::size_t file_count,
|
||||
bool progress_ui_available) noexcept
|
||||
{
|
||||
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||
return {
|
||||
file_count,
|
||||
file_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(file_count),
|
||||
progress_ui_available,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_download_transfer(
|
||||
std::string_view url,
|
||||
std::string_view destination_path,
|
||||
bool has_progress_callback,
|
||||
bool disables_tls_verification) noexcept
|
||||
{
|
||||
if (url.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::reject_missing_source,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
if (destination_path.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::reject_missing_destination,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::start_transfer,
|
||||
has_progress_callback,
|
||||
disables_tls_verification,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_upload_transfer(
|
||||
std::string_view filename,
|
||||
bool has_progress_callback,
|
||||
bool disables_tls_verification) noexcept
|
||||
{
|
||||
if (filename.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::upload,
|
||||
CloudTransferAction::reject_missing_source,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
CloudTransferDirection::upload,
|
||||
CloudTransferAction::start_transfer,
|
||||
has_progress_callback,
|
||||
disables_tls_verification,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferProgressPlan plan_cloud_transfer_progress(
|
||||
std::int64_t total,
|
||||
std::int64_t current) noexcept
|
||||
{
|
||||
if (total <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto clamped_current = current < 0
|
||||
? std::int64_t { 0 }
|
||||
: (current > total ? total : current);
|
||||
return {
|
||||
true,
|
||||
static_cast<float>(static_cast<double>(clamped_current) / static_cast<double>(total)),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
|
||||
const CloudUploadPlan& plan,
|
||||
CloudServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case CloudUploadAction::unavailable_no_canvas:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudUploadAction::show_save_required_warning:
|
||||
services.show_save_required_warning();
|
||||
return pp::foundation::Status::success();
|
||||
case CloudUploadAction::prompt_publish:
|
||||
services.prompt_publish(plan.save_before_upload);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud upload action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_bulk_upload_plan(
|
||||
const CloudBulkUploadPlan& plan,
|
||||
CloudServices& services)
|
||||
{
|
||||
services.begin_bulk_upload(plan.progress_total, plan.show_progress);
|
||||
services.upload_all_bulk_files();
|
||||
services.end_bulk_upload();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_browse_action(
|
||||
CloudBrowseAction action,
|
||||
CloudServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case CloudBrowseAction::unavailable_no_canvas:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudBrowseAction::show_browser:
|
||||
services.show_browser();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud browse action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_download_selection_action(
|
||||
CloudDownloadSelectionAction action,
|
||||
CloudServices& services,
|
||||
const CloudDownloadRequest& request)
|
||||
{
|
||||
switch (action) {
|
||||
case CloudDownloadSelectionAction::wait_for_selection:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudDownloadSelectionAction::start_download:
|
||||
if (request.selected_file.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("cloud download requires a selected file");
|
||||
}
|
||||
services.start_download(request);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud download selection action");
|
||||
}
|
||||
|
||||
}
|
||||
1
src/app_core/document_export.cpp
Normal file
1
src/app_core/document_export.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "app_core/document_export.h"
|
||||
1145
src/app_core/document_export.h
Normal file
1145
src/app_core/document_export.h
Normal file
File diff suppressed because it is too large
Load Diff
88
src/app_core/document_import.h
Normal file
88
src/app_core/document_import.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentImageImportAction {
|
||||
import_equirectangular,
|
||||
place_transform,
|
||||
};
|
||||
|
||||
struct DocumentImageImportPlan {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
DocumentImageImportAction action = DocumentImageImportAction::place_transform;
|
||||
bool imports_equirectangular = false;
|
||||
bool enters_transform_mode = false;
|
||||
};
|
||||
|
||||
class DocumentImageImportServices {
|
||||
public:
|
||||
virtual ~DocumentImageImportServices() = default;
|
||||
|
||||
virtual void import_equirectangular(std::string_view path) = 0;
|
||||
virtual void enter_transform_import(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_document_image_import_dimensions(
|
||||
int width,
|
||||
int height) noexcept
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("image dimensions must be positive");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentImageImportPlan> plan_document_image_import(
|
||||
int width,
|
||||
int height) noexcept
|
||||
{
|
||||
const auto dimensions = validate_document_image_import_dimensions(width, height);
|
||||
if (!dimensions.ok()) {
|
||||
return pp::foundation::Result<DocumentImageImportPlan>::failure(dimensions);
|
||||
}
|
||||
|
||||
const auto wide_equirect = static_cast<long long>(width) == static_cast<long long>(height) * 2LL;
|
||||
const auto vertical_cube_strip = width == height / 6;
|
||||
|
||||
DocumentImageImportPlan plan;
|
||||
plan.width = width;
|
||||
plan.height = height;
|
||||
plan.imports_equirectangular = wide_equirect || vertical_cube_strip;
|
||||
plan.enters_transform_mode = !plan.imports_equirectangular;
|
||||
plan.action = plan.imports_equirectangular
|
||||
? DocumentImageImportAction::import_equirectangular
|
||||
: DocumentImageImportAction::place_transform;
|
||||
return pp::foundation::Result<DocumentImageImportPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_image_import_plan(
|
||||
const DocumentImageImportPlan& plan,
|
||||
std::string_view path,
|
||||
DocumentImageImportServices& services)
|
||||
{
|
||||
const auto dimensions = validate_document_image_import_dimensions(plan.width, plan.height);
|
||||
if (!dimensions.ok()) {
|
||||
return dimensions;
|
||||
}
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("image import path must not be empty");
|
||||
}
|
||||
|
||||
switch (plan.action) {
|
||||
case DocumentImageImportAction::import_equirectangular:
|
||||
services.import_equirectangular(path);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentImageImportAction::place_transform:
|
||||
services.enter_transform_import(path);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown image import action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
760
src/app_core/document_layer.h
Normal file
760
src/app_core/document_layer.h
Normal file
@@ -0,0 +1,760 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr std::size_t document_layer_name_max_length = 128;
|
||||
inline constexpr int document_layer_legacy_blend_mode_count = 5;
|
||||
|
||||
enum class DocumentLayerRenameAction {
|
||||
no_op_same_name,
|
||||
rename_and_record_undo,
|
||||
};
|
||||
|
||||
enum class DocumentLayerOperation {
|
||||
add,
|
||||
duplicate,
|
||||
select,
|
||||
reorder,
|
||||
remove,
|
||||
set_opacity,
|
||||
set_visibility,
|
||||
set_alpha_lock,
|
||||
set_blend_mode,
|
||||
set_highlight,
|
||||
};
|
||||
|
||||
enum class DocumentLayerMenuCommand {
|
||||
clear,
|
||||
rename,
|
||||
merge_down,
|
||||
};
|
||||
|
||||
enum class DocumentLayerMenuAction {
|
||||
clear_current_layer,
|
||||
show_rename_dialog,
|
||||
merge_with_lower_layer,
|
||||
show_merge_animated_not_supported,
|
||||
no_op_select_layer,
|
||||
no_op_select_upper_layer,
|
||||
};
|
||||
|
||||
struct DocumentLayerRenamePlan {
|
||||
std::string old_name;
|
||||
std::string new_name;
|
||||
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
|
||||
};
|
||||
|
||||
struct DocumentLayerOperationPlan {
|
||||
DocumentLayerOperation operation = DocumentLayerOperation::select;
|
||||
int index = 0;
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
int insert_index = 0;
|
||||
int source_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool flag = false;
|
||||
int blend_mode = 0;
|
||||
bool mutates_document = false;
|
||||
bool marks_unsaved = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_title = false;
|
||||
};
|
||||
|
||||
struct DocumentLayerMenuPlan {
|
||||
DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
|
||||
DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
|
||||
std::string label;
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
};
|
||||
|
||||
struct DocumentLayerMergePlan {
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
bool create_history = true;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelInput {
|
||||
int layer_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
int blend_mode = 0;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelLayerView {
|
||||
int layer_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
int blend_mode = 0;
|
||||
bool current = false;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelView {
|
||||
int current_index = 0;
|
||||
float current_opacity = 1.0F;
|
||||
bool current_alpha_locked = false;
|
||||
int current_blend_mode = 0;
|
||||
std::vector<DocumentLayerPanelLayerView> layers;
|
||||
};
|
||||
|
||||
class DocumentLayerMenuServices {
|
||||
public:
|
||||
virtual ~DocumentLayerMenuServices() = default;
|
||||
|
||||
virtual void clear_current_layer() = 0;
|
||||
virtual void show_rename_dialog() = 0;
|
||||
virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
|
||||
virtual void show_merge_animated_not_supported() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerRenameServices {
|
||||
public:
|
||||
virtual ~DocumentLayerRenameServices() = default;
|
||||
|
||||
virtual void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) = 0;
|
||||
virtual void set_current_layer_name(std::string_view new_name) = 0;
|
||||
virtual void finish_layer_rename() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerOperationServices {
|
||||
public:
|
||||
virtual ~DocumentLayerOperationServices() = default;
|
||||
|
||||
virtual void add_layer(std::string_view name, int insert_index) = 0;
|
||||
virtual void duplicate_layer(int source_index, int insert_index) = 0;
|
||||
virtual void select_layer(int index) = 0;
|
||||
virtual void reorder_layer(int from_index, int to_index) = 0;
|
||||
virtual void remove_layer(int index) = 0;
|
||||
virtual void set_layer_opacity(int index, float opacity) = 0;
|
||||
virtual void set_layer_visibility(int index, bool visible) = 0;
|
||||
virtual void set_layer_alpha_lock(int index, bool locked) = 0;
|
||||
virtual void set_layer_blend_mode(int index, int blend_mode) = 0;
|
||||
virtual void set_layer_highlight(int index, bool highlighted) = 0;
|
||||
virtual void mark_unsaved() = 0;
|
||||
virtual void reload_animation_layers() = 0;
|
||||
virtual void update_title() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerMergeServices {
|
||||
public:
|
||||
virtual ~DocumentLayerMergeServices() = default;
|
||||
|
||||
virtual void merge_layers(int from_index, int to_index, bool create_history) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool document_layer_rename_records_history(
|
||||
const DocumentLayerRenamePlan& plan) noexcept
|
||||
{
|
||||
return plan.action == DocumentLayerRenameAction::rename_and_record_undo;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool document_layer_operation_records_history(
|
||||
const DocumentLayerOperationPlan& plan) noexcept
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentLayerOperation::add:
|
||||
case DocumentLayerOperation::duplicate:
|
||||
case DocumentLayerOperation::remove:
|
||||
case DocumentLayerOperation::set_opacity:
|
||||
case DocumentLayerOperation::set_visibility:
|
||||
case DocumentLayerOperation::set_alpha_lock:
|
||||
case DocumentLayerOperation::set_blend_mode:
|
||||
return plan.mutates_document;
|
||||
case DocumentLayerOperation::reorder:
|
||||
return plan.mutates_document;
|
||||
case DocumentLayerOperation::select:
|
||||
case DocumentLayerOperation::set_highlight:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool document_layer_merge_records_history(
|
||||
const DocumentLayerMergePlan& plan) noexcept
|
||||
{
|
||||
return plan.create_history;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||
}
|
||||
|
||||
if (index < 0 || index >= layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count < 0) {
|
||||
return pp::foundation::Status::invalid_argument("layer count must not be negative");
|
||||
}
|
||||
|
||||
if (index < 0 || index > layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerPanelView> plan_document_layer_panel_view(
|
||||
const std::vector<DocumentLayerPanelInput>& layers,
|
||||
int current_index)
|
||||
{
|
||||
if (layers.empty()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer panel requires at least one layer"));
|
||||
}
|
||||
|
||||
const auto current_status = validate_layer_index(static_cast<int>(layers.size()), current_index);
|
||||
if (!current_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(current_status);
|
||||
}
|
||||
|
||||
DocumentLayerPanelView view;
|
||||
view.current_index = current_index;
|
||||
view.layers.reserve(layers.size());
|
||||
|
||||
for (const auto& input : layers) {
|
||||
const auto index_status = validate_layer_index(static_cast<int>(layers.size()), input.layer_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(index_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(input.opacity) || input.opacity < 0.0F || input.opacity > 1.0F) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||
}
|
||||
|
||||
if (input.blend_mode < 0 || input.blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||
}
|
||||
|
||||
const bool current = input.layer_index == current_index;
|
||||
DocumentLayerPanelLayerView layer;
|
||||
layer.layer_index = input.layer_index;
|
||||
layer.name = input.name;
|
||||
layer.opacity = input.opacity;
|
||||
layer.visible = input.visible;
|
||||
layer.alpha_locked = input.alpha_locked;
|
||||
layer.blend_mode = input.blend_mode;
|
||||
layer.current = current;
|
||||
if (current) {
|
||||
view.current_opacity = input.opacity;
|
||||
view.current_alpha_locked = input.alpha_locked;
|
||||
view.current_blend_mode = input.blend_mode;
|
||||
}
|
||||
view.layers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::success(std::move(view));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
|
||||
std::string_view old_name,
|
||||
std::string_view requested_name)
|
||||
{
|
||||
if (requested_name.empty()) {
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer name must not be empty"));
|
||||
}
|
||||
|
||||
if (requested_name.size() > document_layer_name_max_length) {
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
|
||||
}
|
||||
|
||||
DocumentLayerRenamePlan plan;
|
||||
plan.old_name = std::string(old_name);
|
||||
plan.new_name = std::string(requested_name);
|
||||
plan.action = old_name == requested_name
|
||||
? DocumentLayerRenameAction::no_op_same_name
|
||||
: DocumentLayerRenameAction::rename_and_record_undo;
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
|
||||
int layer_count,
|
||||
int insert_index,
|
||||
std::string_view name)
|
||||
{
|
||||
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto rename = plan_document_layer_rename({}, name);
|
||||
if (!rename) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::add;
|
||||
plan.insert_index = insert_index;
|
||||
plan.name = std::string(name);
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
|
||||
int layer_count,
|
||||
int source_index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, source_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::duplicate;
|
||||
plan.source_index = source_index;
|
||||
plan.insert_index = source_index + 1;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::select;
|
||||
plan.index = index;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
|
||||
int layer_count,
|
||||
int from_index,
|
||||
int to_index)
|
||||
{
|
||||
auto index_status = validate_layer_index(layer_count, from_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
index_status = validate_layer_index(layer_count, to_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::reorder;
|
||||
plan.from_index = from_index;
|
||||
plan.to_index = to_index;
|
||||
plan.mutates_document = from_index != to_index;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_title = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (layer_count <= 1) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::remove;
|
||||
plan.index = index;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
|
||||
int layer_count,
|
||||
int index,
|
||||
float opacity)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_opacity;
|
||||
plan.index = index;
|
||||
plan.opacity = opacity;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool visible)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_visibility;
|
||||
plan.index = index;
|
||||
plan.flag = visible;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool locked)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_alpha_lock;
|
||||
plan.index = index;
|
||||
plan.flag = locked;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
|
||||
int layer_count,
|
||||
int index,
|
||||
int blend_mode)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_blend_mode;
|
||||
plan.index = index;
|
||||
plan.blend_mode = blend_mode;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool highlight)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_highlight;
|
||||
plan.index = index;
|
||||
plan.flag = highlight;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
|
||||
DocumentLayerMenuCommand command,
|
||||
bool has_current_layer,
|
||||
int current_index,
|
||||
int animation_duration,
|
||||
std::string_view current_layer_name,
|
||||
std::string_view lower_layer_name)
|
||||
{
|
||||
if (current_index < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current layer index must not be negative"));
|
||||
}
|
||||
if (animation_duration < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation duration must not be negative"));
|
||||
}
|
||||
|
||||
DocumentLayerMenuPlan plan;
|
||||
plan.command = command;
|
||||
plan.from_index = current_index;
|
||||
plan.to_index = current_index > 0 ? current_index - 1 : 0;
|
||||
|
||||
switch (command) {
|
||||
case DocumentLayerMenuCommand::clear:
|
||||
plan.action = has_current_layer
|
||||
? DocumentLayerMenuAction::clear_current_layer
|
||||
: DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = has_current_layer
|
||||
? "Clear Layer " + std::string(current_layer_name)
|
||||
: "Clear Layer (Select a layer)";
|
||||
break;
|
||||
case DocumentLayerMenuCommand::rename:
|
||||
plan.action = has_current_layer
|
||||
? DocumentLayerMenuAction::show_rename_dialog
|
||||
: DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = has_current_layer
|
||||
? "Rename Layer " + std::string(current_layer_name)
|
||||
: "Rename Layer (Select a layer)";
|
||||
break;
|
||||
case DocumentLayerMenuCommand::merge_down:
|
||||
if (!has_current_layer) {
|
||||
plan.action = DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = "Merge Layer (Select a layer)";
|
||||
} else if (animation_duration > 1) {
|
||||
plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
|
||||
plan.label = "Merge Layer (Animation not supported)";
|
||||
} else if (current_index <= 0) {
|
||||
plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
|
||||
plan.label = "Merge Layer (Select upper layers)";
|
||||
} else {
|
||||
plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
|
||||
plan.label = "Merge with " + std::string(lower_layer_name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMergePlan> plan_document_layer_merge(
|
||||
int layer_count,
|
||||
int from_index,
|
||||
int to_index,
|
||||
int animation_duration,
|
||||
bool create_history = true)
|
||||
{
|
||||
auto index_status = validate_layer_index(layer_count, from_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
|
||||
}
|
||||
|
||||
index_status = validate_layer_index(layer_count, to_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (animation_duration < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation duration must not be negative"));
|
||||
}
|
||||
|
||||
if (animation_duration > 1) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animated layer merge is not supported"));
|
||||
}
|
||||
|
||||
if (from_index <= to_index) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer merge source must be above the destination"));
|
||||
}
|
||||
|
||||
DocumentLayerMergePlan plan;
|
||||
plan.from_index = from_index;
|
||||
plan.to_index = to_index;
|
||||
plan.create_history = create_history;
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_rename_plan(
|
||||
const DocumentLayerRenamePlan& plan,
|
||||
DocumentLayerRenameServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case DocumentLayerRenameAction::no_op_same_name:
|
||||
services.finish_layer_rename();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerRenameAction::rename_and_record_undo:
|
||||
if (plan.new_name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("layer rename plan must include a new name");
|
||||
}
|
||||
if (plan.old_name == plan.new_name) {
|
||||
return pp::foundation::Status::invalid_argument("layer rename plan must change the name");
|
||||
}
|
||||
if (!document_layer_rename_records_history(plan)) {
|
||||
return pp::foundation::Status::invalid_argument(
|
||||
"layer rename plan must record history when the name changes");
|
||||
}
|
||||
services.record_layer_rename_undo(plan.old_name, plan.new_name);
|
||||
services.set_current_layer_name(plan.new_name);
|
||||
services.finish_layer_rename();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document layer rename action");
|
||||
}
|
||||
|
||||
inline void execute_document_layer_operation_side_effects(
|
||||
const DocumentLayerOperationPlan& plan,
|
||||
DocumentLayerOperationServices& services)
|
||||
{
|
||||
if (plan.marks_unsaved)
|
||||
services.mark_unsaved();
|
||||
if (plan.reloads_animation_layers)
|
||||
services.reload_animation_layers();
|
||||
if (plan.updates_title)
|
||||
services.update_title();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_operation_plan(
|
||||
const DocumentLayerOperationPlan& plan,
|
||||
DocumentLayerOperationServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentLayerOperation::add:
|
||||
if (!plan.mutates_document || plan.name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("layer add plan must mutate with a name");
|
||||
}
|
||||
services.add_layer(plan.name, plan.insert_index);
|
||||
break;
|
||||
case DocumentLayerOperation::duplicate:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer duplicate plan must mutate the document");
|
||||
}
|
||||
services.duplicate_layer(plan.source_index, plan.insert_index);
|
||||
break;
|
||||
case DocumentLayerOperation::select:
|
||||
services.select_layer(plan.index);
|
||||
break;
|
||||
case DocumentLayerOperation::reorder:
|
||||
if (plan.mutates_document)
|
||||
services.reorder_layer(plan.from_index, plan.to_index);
|
||||
break;
|
||||
case DocumentLayerOperation::remove:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer remove plan must mutate the document");
|
||||
}
|
||||
services.remove_layer(plan.index);
|
||||
break;
|
||||
case DocumentLayerOperation::set_opacity:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer opacity plan must mutate the document");
|
||||
}
|
||||
services.set_layer_opacity(plan.index, plan.opacity);
|
||||
break;
|
||||
case DocumentLayerOperation::set_visibility:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer visibility plan must mutate the document");
|
||||
}
|
||||
services.set_layer_visibility(plan.index, plan.flag);
|
||||
break;
|
||||
case DocumentLayerOperation::set_alpha_lock:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer alpha-lock plan must mutate the document");
|
||||
}
|
||||
services.set_layer_alpha_lock(plan.index, plan.flag);
|
||||
break;
|
||||
case DocumentLayerOperation::set_blend_mode:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer blend-mode plan must mutate the document");
|
||||
}
|
||||
services.set_layer_blend_mode(plan.index, plan.blend_mode);
|
||||
break;
|
||||
case DocumentLayerOperation::set_highlight:
|
||||
services.set_layer_highlight(plan.index, plan.flag);
|
||||
break;
|
||||
}
|
||||
|
||||
execute_document_layer_operation_side_effects(plan, services);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_merge_plan(
|
||||
const DocumentLayerMergePlan& plan,
|
||||
DocumentLayerMergeServices& services)
|
||||
{
|
||||
if (plan.from_index <= plan.to_index) {
|
||||
return pp::foundation::Status::invalid_argument(
|
||||
"layer merge source must be above the destination");
|
||||
}
|
||||
|
||||
services.merge_layers(plan.from_index, plan.to_index, plan.create_history);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
|
||||
const DocumentLayerMenuPlan& plan,
|
||||
DocumentLayerMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case DocumentLayerMenuAction::clear_current_layer:
|
||||
services.clear_current_layer();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::show_rename_dialog:
|
||||
services.show_rename_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::merge_with_lower_layer:
|
||||
services.merge_with_lower_layer(plan.from_index, plan.to_index);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::show_merge_animated_not_supported:
|
||||
services.show_merge_animated_not_supported();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::no_op_select_layer:
|
||||
case DocumentLayerMenuAction::no_op_select_upper_layer:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document layer menu action");
|
||||
}
|
||||
|
||||
}
|
||||
73
src/app_core/document_platform_io.h
Normal file
73
src/app_core/document_platform_io.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class PickedPathAction {
|
||||
ignore_empty_path,
|
||||
invoke_callback,
|
||||
};
|
||||
|
||||
enum class DisplayFileAction {
|
||||
ignore_empty_path,
|
||||
open_external_file,
|
||||
};
|
||||
|
||||
enum class VirtualKeyboardAction {
|
||||
show_keyboard,
|
||||
hide_keyboard,
|
||||
};
|
||||
|
||||
enum class CursorVisibilityAction {
|
||||
show_cursor,
|
||||
hide_cursor,
|
||||
};
|
||||
|
||||
enum class ClipboardReadAction {
|
||||
read_text,
|
||||
};
|
||||
|
||||
enum class ClipboardWriteAction {
|
||||
write_text,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? PickedPathAction::ignore_empty_path
|
||||
: PickedPathAction::invoke_callback;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? DisplayFileAction::ignore_empty_path
|
||||
: DisplayFileAction::open_external_file;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr VirtualKeyboardAction plan_virtual_keyboard(bool visible) noexcept
|
||||
{
|
||||
return visible
|
||||
? VirtualKeyboardAction::show_keyboard
|
||||
: VirtualKeyboardAction::hide_keyboard;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept
|
||||
{
|
||||
return visible
|
||||
? CursorVisibilityAction::show_cursor
|
||||
: CursorVisibilityAction::hide_cursor;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
|
||||
{
|
||||
return ClipboardReadAction::read_text;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
|
||||
{
|
||||
return ClipboardWriteAction::write_text;
|
||||
}
|
||||
|
||||
}
|
||||
164
src/app_core/document_recording.h
Normal file
164
src/app_core/document_recording.h
Normal file
@@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class RecordingStartAction {
|
||||
start_thread,
|
||||
no_op_already_running,
|
||||
};
|
||||
|
||||
enum class RecordingStopAction {
|
||||
stop_thread,
|
||||
no_op_not_running,
|
||||
};
|
||||
|
||||
struct RecordingClearPlan {
|
||||
bool stop_running_recording = false;
|
||||
bool delete_recorded_files = false;
|
||||
int frame_count_after_clear = 0;
|
||||
};
|
||||
|
||||
struct RecordingExportPlan {
|
||||
std::size_t frame_count = 0;
|
||||
int progress_total = 0;
|
||||
};
|
||||
|
||||
struct RecordingWorkerIterationPlan {
|
||||
bool continue_running = true;
|
||||
bool encode_frame = false;
|
||||
bool clear_dirty_stroke = false;
|
||||
bool update_frame_label = false;
|
||||
};
|
||||
|
||||
class RecordingServices {
|
||||
public:
|
||||
virtual ~RecordingServices() = default;
|
||||
|
||||
virtual void start_thread() = 0;
|
||||
virtual void stop_thread() = 0;
|
||||
virtual void delete_recorded_files() = 0;
|
||||
virtual void set_frame_count(int frame_count) = 0;
|
||||
virtual void update_frame_label() = 0;
|
||||
virtual void begin_export(int progress_total) = 0;
|
||||
virtual void write_mp4(std::string_view path) = 0;
|
||||
virtual void end_export() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
|
||||
{
|
||||
return is_running
|
||||
? RecordingStartAction::no_op_already_running
|
||||
: RecordingStartAction::start_thread;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingStopAction plan_recording_stop(bool is_running) noexcept
|
||||
{
|
||||
return is_running
|
||||
? RecordingStopAction::stop_thread
|
||||
: RecordingStopAction::no_op_not_running;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingClearPlan plan_recording_clear(
|
||||
bool is_running,
|
||||
bool platform_deletes_recorded_files) noexcept
|
||||
{
|
||||
return {
|
||||
is_running,
|
||||
platform_deletes_recorded_files,
|
||||
0,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingExportPlan plan_recording_export(std::size_t frame_count) noexcept
|
||||
{
|
||||
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||
return {
|
||||
frame_count,
|
||||
frame_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(frame_count),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_recording_export_progress_dialog(
|
||||
const RecordingExportPlan& plan)
|
||||
{
|
||||
return plan_app_progress_dialog("Exporting MP4 movie", plan.progress_total);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingWorkerIterationPlan plan_recording_worker_iteration(
|
||||
bool is_running_after_wake,
|
||||
bool has_encoder,
|
||||
bool has_canvas_document) noexcept
|
||||
{
|
||||
const bool encode = is_running_after_wake && has_encoder && has_canvas_document;
|
||||
return {
|
||||
.continue_running = is_running_after_wake,
|
||||
.encode_frame = encode,
|
||||
.clear_dirty_stroke = encode,
|
||||
.update_frame_label = encode,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_start_action(
|
||||
RecordingStartAction action,
|
||||
RecordingServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case RecordingStartAction::start_thread:
|
||||
services.start_thread();
|
||||
return pp::foundation::Status::success();
|
||||
case RecordingStartAction::no_op_already_running:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown recording start action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_stop_action(
|
||||
RecordingStopAction action,
|
||||
RecordingServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case RecordingStopAction::stop_thread:
|
||||
services.stop_thread();
|
||||
return pp::foundation::Status::success();
|
||||
case RecordingStopAction::no_op_not_running:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown recording stop action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_clear_plan(
|
||||
const RecordingClearPlan& plan,
|
||||
RecordingServices& services)
|
||||
{
|
||||
if (plan.stop_running_recording) {
|
||||
services.stop_thread();
|
||||
}
|
||||
if (plan.delete_recorded_files) {
|
||||
services.delete_recorded_files();
|
||||
}
|
||||
services.set_frame_count(plan.frame_count_after_clear);
|
||||
services.update_frame_label();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_export_plan(
|
||||
const RecordingExportPlan& plan,
|
||||
RecordingServices& services,
|
||||
std::string_view path)
|
||||
{
|
||||
services.begin_export(plan.progress_total);
|
||||
services.write_mp4(path);
|
||||
services.end_export();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
82
src/app_core/document_resize.h
Normal file
82
src/app_core/document_resize.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_status.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct DocumentResizeDialogState {
|
||||
int current_resolution = 0;
|
||||
std::string current_resolution_text;
|
||||
int current_resolution_index = 0;
|
||||
};
|
||||
|
||||
struct DocumentResizePlan {
|
||||
int resolution = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool clears_history = false;
|
||||
};
|
||||
|
||||
class DocumentResizeServices {
|
||||
public:
|
||||
virtual ~DocumentResizeServices() = default;
|
||||
|
||||
virtual void resize_document(int width, int height) = 0;
|
||||
virtual void update_title() = 0;
|
||||
virtual void clear_history() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline DocumentResizeDialogState make_document_resize_dialog_state(
|
||||
int current_resolution)
|
||||
{
|
||||
const auto label = document_resolution_label(current_resolution);
|
||||
const auto index = document_resolution_to_index(current_resolution);
|
||||
std::string text = "Current: ";
|
||||
text.append(label ? std::string_view(label.value()) : std::string_view("unknown"));
|
||||
|
||||
return {
|
||||
current_resolution,
|
||||
text,
|
||||
index ? static_cast<int>(index.value()) : static_cast<int>(document_resolution_values.size()),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentResizePlan> plan_document_resize(
|
||||
int selected_resolution_index)
|
||||
{
|
||||
const auto resolution = display_resolution_from_index(selected_resolution_index);
|
||||
if (!resolution) {
|
||||
return pp::foundation::Result<DocumentResizePlan>::failure(resolution.status());
|
||||
}
|
||||
|
||||
const auto value = resolution.value();
|
||||
return pp::foundation::Result<DocumentResizePlan>::success(
|
||||
DocumentResizePlan {
|
||||
value,
|
||||
value,
|
||||
value,
|
||||
true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_resize_plan(
|
||||
const DocumentResizePlan& plan,
|
||||
DocumentResizeServices& services)
|
||||
{
|
||||
if (plan.width <= 0 || plan.height <= 0) {
|
||||
return pp::foundation::Status::out_of_range("resize dimensions must be positive");
|
||||
}
|
||||
|
||||
services.resize_document(plan.width, plan.height);
|
||||
services.update_title();
|
||||
if (plan.clears_history) {
|
||||
services.clear_history();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
67
src/app_core/document_route.cpp
Normal file
67
src/app_core/document_route.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "app_core/document_route.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool is_extension_char(char value) noexcept
|
||||
{
|
||||
const auto ch = static_cast<unsigned char>(value);
|
||||
return std::isalnum(ch) != 0 || value == '_';
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
|
||||
{
|
||||
std::string lowered;
|
||||
lowered.reserve(value.size());
|
||||
for (const char ch : value) {
|
||||
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
}
|
||||
return lowered;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
|
||||
{
|
||||
const auto separator = path.find_last_of("/\\");
|
||||
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
|
||||
}
|
||||
|
||||
const auto dot = path.find_last_of('.');
|
||||
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document path must include a file extension"));
|
||||
}
|
||||
|
||||
const std::string_view extension = path.substr(dot + 1U);
|
||||
for (const char ch : extension) {
|
||||
if (!is_extension_char(ch)) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
|
||||
}
|
||||
}
|
||||
|
||||
auto lowered_extension = lowercase_ascii(extension);
|
||||
auto kind = DocumentOpenKind::open_project;
|
||||
if (lowered_extension == "abr") {
|
||||
kind = DocumentOpenKind::import_abr;
|
||||
} else if (lowered_extension == "ppbr") {
|
||||
kind = DocumentOpenKind::import_ppbr;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentOpenRoute>::success(
|
||||
DocumentOpenRoute {
|
||||
.kind = kind,
|
||||
.path = std::string(path),
|
||||
.directory = std::string(path.substr(0U, separator)),
|
||||
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
|
||||
.extension = std::move(lowered_extension),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
27
src/app_core/document_route.h
Normal file
27
src/app_core/document_route.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentOpenKind {
|
||||
import_abr,
|
||||
import_ppbr,
|
||||
open_project,
|
||||
};
|
||||
|
||||
struct DocumentOpenRoute {
|
||||
DocumentOpenKind kind = DocumentOpenKind::open_project;
|
||||
std::string path;
|
||||
std::string directory;
|
||||
std::string name;
|
||||
std::string extension;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
|
||||
std::string_view path);
|
||||
|
||||
}
|
||||
62
src/app_core/document_session.cpp
Normal file
62
src/app_core/document_session.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "app_core/document_session.h"
|
||||
|
||||
namespace pp::app {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] constexpr HistoryUiPlan make_history_clear_effect() noexcept
|
||||
{
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.clears_history = true;
|
||||
plan.updates_memory_label = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr HistoryUiPlan make_history_no_op_effect() noexcept
|
||||
{
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.no_op = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HistoryUiPlan plan_document_open_history(const DocumentOpenRoute& route) noexcept
|
||||
{
|
||||
return route.kind == DocumentOpenKind::open_project
|
||||
? make_history_clear_effect()
|
||||
: make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_close_request_history(CloseRequestDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_save_history(DocumentSaveDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_workflow_history(DocumentWorkflowDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_file_save_history(const DocumentFileSavePlan&) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_version_save_history(const DocumentVersionTarget&) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_new_document_history(const NewDocumentPlan&) noexcept
|
||||
{
|
||||
return make_history_clear_effect();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
576
src/app_core/document_session.h
Normal file
576
src/app_core/document_session.h
Normal file
@@ -0,0 +1,576 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_route.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class ProjectOpenDecision {
|
||||
open_now,
|
||||
prompt_discard_unsaved,
|
||||
};
|
||||
|
||||
enum class CloseRequestDecision {
|
||||
close_now,
|
||||
show_unsaved_prompt,
|
||||
wait_for_existing_prompt,
|
||||
};
|
||||
|
||||
enum class DocumentSaveIntent {
|
||||
save,
|
||||
save_as,
|
||||
save_version,
|
||||
save_dirty_version,
|
||||
};
|
||||
|
||||
enum class DocumentSaveDecision {
|
||||
no_op,
|
||||
show_save_dialog,
|
||||
save_existing,
|
||||
save_version,
|
||||
};
|
||||
|
||||
enum class DocumentWorkflowDecision {
|
||||
unavailable,
|
||||
continue_now,
|
||||
prompt_save_before_continue,
|
||||
};
|
||||
|
||||
enum class DocumentFileWriteDecision {
|
||||
save_now,
|
||||
prompt_overwrite,
|
||||
};
|
||||
|
||||
enum class DocumentOpenPlanAction {
|
||||
open_project_now,
|
||||
prompt_discard_unsaved_project,
|
||||
prompt_import_abr,
|
||||
prompt_import_ppbr,
|
||||
};
|
||||
|
||||
enum class DocumentSessionPromptKind {
|
||||
close_unsaved_document,
|
||||
save_before_workflow_continue,
|
||||
new_document_overwrite,
|
||||
document_file_overwrite,
|
||||
document_save_error,
|
||||
};
|
||||
|
||||
class DocumentOpenServices {
|
||||
public:
|
||||
virtual ~DocumentOpenServices() = default;
|
||||
|
||||
virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0;
|
||||
virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0;
|
||||
virtual void open_project_now(const DocumentOpenRoute& route) = 0;
|
||||
virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0;
|
||||
};
|
||||
|
||||
class CloseRequestServices {
|
||||
public:
|
||||
virtual ~CloseRequestServices() = default;
|
||||
|
||||
virtual void request_close_now() = 0;
|
||||
virtual void show_unsaved_close_prompt() = 0;
|
||||
};
|
||||
|
||||
class DocumentSaveServices {
|
||||
public:
|
||||
virtual ~DocumentSaveServices() = default;
|
||||
|
||||
virtual void show_save_dialog() = 0;
|
||||
virtual void save_existing_document() = 0;
|
||||
virtual void save_document_version() = 0;
|
||||
};
|
||||
|
||||
class DocumentWorkflowServices {
|
||||
public:
|
||||
virtual ~DocumentWorkflowServices() = default;
|
||||
|
||||
virtual void continue_workflow_now() = 0;
|
||||
virtual void prompt_save_before_continue() = 0;
|
||||
};
|
||||
|
||||
struct DocumentFileTarget {
|
||||
std::string name;
|
||||
std::string directory;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentVersionTarget {
|
||||
std::string name;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentFileSavePlan {
|
||||
DocumentFileTarget target;
|
||||
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||
};
|
||||
|
||||
class DocumentFileSaveServices {
|
||||
public:
|
||||
virtual ~DocumentFileSaveServices() = default;
|
||||
|
||||
virtual void save_document_file(const DocumentFileSavePlan& plan) = 0;
|
||||
virtual void prompt_overwrite_document_file(const DocumentFileSavePlan& plan) = 0;
|
||||
};
|
||||
|
||||
class DocumentVersionSaveServices {
|
||||
public:
|
||||
virtual ~DocumentVersionSaveServices() = default;
|
||||
|
||||
virtual void save_document_version(const DocumentVersionTarget& target) = 0;
|
||||
};
|
||||
|
||||
struct NewDocumentPlan {
|
||||
DocumentFileTarget target;
|
||||
int resolution = 0;
|
||||
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||
};
|
||||
|
||||
class NewDocumentServices {
|
||||
public:
|
||||
virtual ~NewDocumentServices() = default;
|
||||
|
||||
virtual void create_new_document(const NewDocumentPlan& plan) = 0;
|
||||
virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] HistoryUiPlan plan_document_open_history(const DocumentOpenRoute& route) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_close_request_history(CloseRequestDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_save_history(DocumentSaveDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_workflow_history(DocumentWorkflowDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_file_save_history(const DocumentFileSavePlan& plan) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_version_save_history(const DocumentVersionTarget& target) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_new_document_history(const NewDocumentPlan& plan) noexcept;
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt(
|
||||
DocumentSessionPromptKind kind,
|
||||
std::string_view document_name = {})
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentSessionPromptKind::close_unsaved_document:
|
||||
return plan_app_message_dialog(
|
||||
"Unsaved document",
|
||||
"Do you want to close without saving?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
case DocumentSessionPromptKind::save_before_workflow_continue:
|
||||
return plan_app_message_dialog(
|
||||
"Unsaved document",
|
||||
"Would you like to save this document before closing?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
case DocumentSessionPromptKind::new_document_overwrite:
|
||||
return plan_app_message_dialog(
|
||||
"Warning",
|
||||
"A document with this name already exists, continue?",
|
||||
true);
|
||||
case DocumentSessionPromptKind::document_file_overwrite:
|
||||
{
|
||||
std::string message = "Are you sure you want to overwrite ";
|
||||
message += document_name;
|
||||
message += "?";
|
||||
return plan_app_message_dialog("Warning", message, true);
|
||||
}
|
||||
case DocumentSessionPromptKind::document_save_error:
|
||||
return plan_app_message_dialog(
|
||||
"Saving Error",
|
||||
"There was a problem saving the document",
|
||||
false);
|
||||
}
|
||||
|
||||
return plan_app_message_dialog("Warning", "", false);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
|
||||
{
|
||||
return has_unsaved_changes
|
||||
? ProjectOpenDecision::prompt_discard_unsaved
|
||||
: ProjectOpenDecision::open_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
|
||||
DocumentOpenKind kind,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentOpenKind::import_abr:
|
||||
return DocumentOpenPlanAction::prompt_import_abr;
|
||||
case DocumentOpenKind::import_ppbr:
|
||||
return DocumentOpenPlanAction::prompt_import_ppbr;
|
||||
case DocumentOpenKind::open_project:
|
||||
return has_unsaved_changes
|
||||
? DocumentOpenPlanAction::prompt_discard_unsaved_project
|
||||
: DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
return DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_open_plan(
|
||||
DocumentOpenPlanAction action,
|
||||
const DocumentOpenRoute& route,
|
||||
DocumentOpenServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case DocumentOpenPlanAction::open_project_now:
|
||||
if (route.kind != DocumentOpenKind::open_project) {
|
||||
return pp::foundation::Status::invalid_argument("open-project action requires a project route");
|
||||
}
|
||||
services.open_project_now(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_discard_unsaved_project:
|
||||
if (route.kind != DocumentOpenKind::open_project) {
|
||||
return pp::foundation::Status::invalid_argument("discard prompt requires a project route");
|
||||
}
|
||||
services.prompt_discard_unsaved_project(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_import_abr:
|
||||
if (route.kind != DocumentOpenKind::import_abr) {
|
||||
return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route");
|
||||
}
|
||||
services.prompt_import_abr(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_import_ppbr:
|
||||
if (route.kind != DocumentOpenKind::import_ppbr) {
|
||||
return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route");
|
||||
}
|
||||
services.prompt_import_ppbr(route);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document open action");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
|
||||
bool has_unsaved_changes,
|
||||
bool close_prompt_already_open) noexcept
|
||||
{
|
||||
if (!has_unsaved_changes) {
|
||||
return CloseRequestDecision::close_now;
|
||||
}
|
||||
|
||||
return close_prompt_already_open
|
||||
? CloseRequestDecision::wait_for_existing_prompt
|
||||
: CloseRequestDecision::show_unsaved_prompt;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_close_request_decision(
|
||||
CloseRequestDecision decision,
|
||||
CloseRequestServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case CloseRequestDecision::close_now:
|
||||
services.request_close_now();
|
||||
return pp::foundation::Status::success();
|
||||
case CloseRequestDecision::show_unsaved_prompt:
|
||||
services.show_unsaved_close_prompt();
|
||||
return pp::foundation::Status::success();
|
||||
case CloseRequestDecision::wait_for_existing_prompt:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown close request decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
|
||||
bool is_new_document,
|
||||
bool has_unsaved_changes,
|
||||
DocumentSaveIntent intent) noexcept
|
||||
{
|
||||
switch (intent) {
|
||||
case DocumentSaveIntent::save:
|
||||
if (is_new_document) {
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
}
|
||||
return has_unsaved_changes
|
||||
? DocumentSaveDecision::save_existing
|
||||
: DocumentSaveDecision::no_op;
|
||||
case DocumentSaveIntent::save_as:
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
case DocumentSaveIntent::save_version:
|
||||
return is_new_document
|
||||
? DocumentSaveDecision::show_save_dialog
|
||||
: DocumentSaveDecision::save_version;
|
||||
case DocumentSaveIntent::save_dirty_version:
|
||||
if (is_new_document) {
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
}
|
||||
return has_unsaved_changes
|
||||
? DocumentSaveDecision::save_version
|
||||
: DocumentSaveDecision::no_op;
|
||||
}
|
||||
|
||||
return DocumentSaveDecision::no_op;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_save_decision(
|
||||
DocumentSaveDecision decision,
|
||||
DocumentSaveServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case DocumentSaveDecision::no_op:
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::show_save_dialog:
|
||||
services.show_save_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::save_existing:
|
||||
services.save_existing_document();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::save_version:
|
||||
services.save_document_version();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document save decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
|
||||
bool has_canvas,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
if (!has_canvas) {
|
||||
return DocumentWorkflowDecision::unavailable;
|
||||
}
|
||||
|
||||
return has_unsaved_changes
|
||||
? DocumentWorkflowDecision::prompt_save_before_continue
|
||||
: DocumentWorkflowDecision::continue_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_workflow_decision(
|
||||
DocumentWorkflowDecision decision,
|
||||
DocumentWorkflowServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case DocumentWorkflowDecision::unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentWorkflowDecision::continue_now:
|
||||
services.continue_workflow_now();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentWorkflowDecision::prompt_save_before_continue:
|
||||
services.prompt_save_before_continue();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document workflow decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name)
|
||||
{
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentFileTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
DocumentFileTarget target;
|
||||
target.name = std::string(document_name);
|
||||
target.directory = std::string(work_directory);
|
||||
target.path.reserve(target.directory.size() + target.name.size() + 5);
|
||||
target.path += target.directory;
|
||||
target.path += "/";
|
||||
target.path += target.name;
|
||||
target.path += ".ppi";
|
||||
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
|
||||
bool target_exists) noexcept
|
||||
{
|
||||
return target_exists
|
||||
? DocumentFileWriteDecision::prompt_overwrite
|
||||
: DocumentFileWriteDecision::save_now;
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
auto target = make_document_file_target(work_directory, document_name);
|
||||
if (!target) {
|
||||
return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
|
||||
}
|
||||
|
||||
DocumentFileSavePlan plan;
|
||||
plan.target = std::move(target.value());
|
||||
plan.write_decision = plan_document_file_write(exists(plan.target.path));
|
||||
return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_file_save_plan(
|
||||
const DocumentFileSavePlan& plan,
|
||||
DocumentFileSaveServices& services)
|
||||
{
|
||||
switch (plan.write_decision) {
|
||||
case DocumentFileWriteDecision::save_now:
|
||||
services.save_document_file(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentFileWriteDecision::prompt_overwrite:
|
||||
services.prompt_overwrite_document_file(plan);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document file save write decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
|
||||
{
|
||||
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
||||
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
|
||||
return pp::foundation::Result<int>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name,
|
||||
int resolution_index,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
const auto resolution = document_resolution_from_index(resolution_index);
|
||||
if (!resolution) {
|
||||
return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
|
||||
}
|
||||
|
||||
auto save_plan = plan_document_file_save(
|
||||
work_directory,
|
||||
document_name,
|
||||
std::forward<ExistsPredicate>(exists));
|
||||
if (!save_plan) {
|
||||
return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
|
||||
}
|
||||
|
||||
NewDocumentPlan plan;
|
||||
plan.target = std::move(save_plan.value().target);
|
||||
plan.resolution = resolution.value();
|
||||
plan.write_decision = save_plan.value().write_decision;
|
||||
return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_new_document_plan(
|
||||
const NewDocumentPlan& plan,
|
||||
NewDocumentServices& services)
|
||||
{
|
||||
switch (plan.write_decision) {
|
||||
case DocumentFileWriteDecision::save_now:
|
||||
services.create_new_document(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentFileWriteDecision::prompt_overwrite:
|
||||
services.prompt_overwrite_new_document(plan);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown new document write decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
|
||||
{
|
||||
const auto dot = document_name.rfind('.');
|
||||
if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto is_word = [](char ch) noexcept {
|
||||
return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
|
||||
};
|
||||
return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
|
||||
{
|
||||
int value = 0;
|
||||
for (const char ch : suffix) {
|
||||
if (ch < '0' || ch > '9') {
|
||||
break;
|
||||
}
|
||||
|
||||
value = value * 10 + (ch - '0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
|
||||
{
|
||||
char suffix[4] {};
|
||||
std::snprintf(suffix, sizeof(suffix), ".%02d", version);
|
||||
std::string name;
|
||||
name.reserve(base_name.size() + 3);
|
||||
name += base_name;
|
||||
name += suffix;
|
||||
return name;
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
|
||||
std::string_view directory,
|
||||
std::string_view document_name,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
if (directory.empty()) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("directory must not be empty"));
|
||||
}
|
||||
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
int current = 0;
|
||||
std::string_view base = document_name;
|
||||
if (has_legacy_two_character_version_suffix(document_name)) {
|
||||
const auto dot = document_name.rfind('.');
|
||||
base = document_name.substr(0, dot);
|
||||
current = legacy_version_number(document_name.substr(dot + 1));
|
||||
}
|
||||
|
||||
for (int version = current + 1; version < 99; ++version) {
|
||||
DocumentVersionTarget target;
|
||||
target.name = make_legacy_version_name(base, version);
|
||||
target.path.reserve(directory.size() + target.name.size() + 5);
|
||||
target.path += directory;
|
||||
target.path += "/";
|
||||
target.path += target.name;
|
||||
target.path += ".ppi";
|
||||
if (!exists(target.path)) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::out_of_range("no available document version target"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_version_save(
|
||||
const DocumentVersionTarget& target,
|
||||
DocumentVersionSaveServices& services)
|
||||
{
|
||||
if (target.name.empty() || target.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document version target requires a name and path");
|
||||
}
|
||||
|
||||
services.save_document_version(target);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
19
src/app_core/document_sharing.h
Normal file
19
src/app_core/document_sharing.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentShareAction {
|
||||
show_save_required_warning,
|
||||
share_now,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? DocumentShareAction::show_save_required_warning
|
||||
: DocumentShareAction::share_now;
|
||||
}
|
||||
|
||||
}
|
||||
209
src/app_core/file_menu.h
Normal file
209
src/app_core/file_menu.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/document_export.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class FileMenuCommand {
|
||||
new_document,
|
||||
import_image,
|
||||
open_project,
|
||||
browse_cloud,
|
||||
save,
|
||||
save_as,
|
||||
save_version,
|
||||
export_jpeg,
|
||||
export_submenu,
|
||||
share,
|
||||
resize,
|
||||
cloud_upload,
|
||||
cloud_browse,
|
||||
};
|
||||
|
||||
enum class FileMenuAction {
|
||||
show_new_document_dialog,
|
||||
pick_image_for_import,
|
||||
pick_project_file,
|
||||
show_cloud_browser_dialog,
|
||||
save_document,
|
||||
show_export_jpeg_dialog,
|
||||
show_export_submenu,
|
||||
share_document,
|
||||
show_resize_dialog,
|
||||
upload_to_cloud,
|
||||
browse_cloud_documents,
|
||||
};
|
||||
|
||||
struct FileMenuPlan {
|
||||
FileMenuCommand command = FileMenuCommand::new_document;
|
||||
FileMenuAction action = FileMenuAction::show_new_document_dialog;
|
||||
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
|
||||
DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg;
|
||||
};
|
||||
|
||||
class FileMenuServices {
|
||||
public:
|
||||
virtual ~FileMenuServices() = default;
|
||||
|
||||
virtual void show_new_document_dialog() = 0;
|
||||
virtual void pick_image_for_import() = 0;
|
||||
virtual void pick_project_file() = 0;
|
||||
virtual void show_cloud_browser_dialog() = 0;
|
||||
virtual void save_document(DocumentSaveIntent intent) = 0;
|
||||
virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0;
|
||||
virtual void show_export_submenu() = 0;
|
||||
virtual void share_document() = 0;
|
||||
virtual void show_resize_dialog() = 0;
|
||||
virtual void upload_to_cloud() = 0;
|
||||
virtual void browse_cloud_documents() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept
|
||||
{
|
||||
FileMenuPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case FileMenuCommand::new_document:
|
||||
plan.action = FileMenuAction::show_new_document_dialog;
|
||||
break;
|
||||
case FileMenuCommand::import_image:
|
||||
plan.action = FileMenuAction::pick_image_for_import;
|
||||
break;
|
||||
case FileMenuCommand::open_project:
|
||||
plan.action = FileMenuAction::pick_project_file;
|
||||
break;
|
||||
case FileMenuCommand::browse_cloud:
|
||||
plan.action = FileMenuAction::show_cloud_browser_dialog;
|
||||
break;
|
||||
case FileMenuCommand::save:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save;
|
||||
break;
|
||||
case FileMenuCommand::save_as:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save_as;
|
||||
break;
|
||||
case FileMenuCommand::save_version:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save_version;
|
||||
break;
|
||||
case FileMenuCommand::export_jpeg:
|
||||
plan.action = FileMenuAction::show_export_jpeg_dialog;
|
||||
plan.export_kind = DocumentExportMenuKind::jpeg;
|
||||
break;
|
||||
case FileMenuCommand::export_submenu:
|
||||
plan.action = FileMenuAction::show_export_submenu;
|
||||
break;
|
||||
case FileMenuCommand::share:
|
||||
plan.action = FileMenuAction::share_document;
|
||||
break;
|
||||
case FileMenuCommand::resize:
|
||||
plan.action = FileMenuAction::show_resize_dialog;
|
||||
break;
|
||||
case FileMenuCommand::cloud_upload:
|
||||
plan.action = FileMenuAction::upload_to_cloud;
|
||||
break;
|
||||
case FileMenuCommand::cloud_browse:
|
||||
plan.action = FileMenuAction::browse_cloud_documents;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<FileMenuCommand> parse_file_menu_command(
|
||||
std::string_view command) noexcept
|
||||
{
|
||||
if (command == "new" || command == "new-document") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::new_document);
|
||||
}
|
||||
if (command == "import" || command == "import-image") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::import_image);
|
||||
}
|
||||
if (command == "open" || command == "open-project") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::open_project);
|
||||
}
|
||||
if (command == "browse") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::browse_cloud);
|
||||
}
|
||||
if (command == "save") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save);
|
||||
}
|
||||
if (command == "save-as") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_as);
|
||||
}
|
||||
if (command == "save-version") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_version);
|
||||
}
|
||||
if (command == "export" || command == "export-jpeg") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_jpeg);
|
||||
}
|
||||
if (command == "export-submenu") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_submenu);
|
||||
}
|
||||
if (command == "share") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::share);
|
||||
}
|
||||
if (command == "resize") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::resize);
|
||||
}
|
||||
if (command == "cloud-upload") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_upload);
|
||||
}
|
||||
if (command == "cloud-browse") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_browse);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<FileMenuCommand>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown file menu command"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan(
|
||||
const FileMenuPlan& plan,
|
||||
FileMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case FileMenuAction::show_new_document_dialog:
|
||||
services.show_new_document_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::pick_image_for_import:
|
||||
services.pick_image_for_import();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::pick_project_file:
|
||||
services.pick_project_file();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_cloud_browser_dialog:
|
||||
services.show_cloud_browser_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::save_document:
|
||||
services.save_document(plan.save_intent);
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_export_jpeg_dialog:
|
||||
services.show_export_jpeg_dialog(plan.export_kind);
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_export_submenu:
|
||||
services.show_export_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::share_document:
|
||||
services.share_document();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_resize_dialog:
|
||||
services.show_resize_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::upload_to_cloud:
|
||||
services.upload_to_cloud();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::browse_cloud_documents:
|
||||
services.browse_cloud_documents();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown file menu action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
209
src/app_core/grid_ui.h
Normal file
209
src/app_core/grid_ui.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class GridUiOperation {
|
||||
request_heightmap_pick,
|
||||
load_heightmap,
|
||||
clear_heightmap,
|
||||
reload_heightmap,
|
||||
render_lightmap,
|
||||
commit_heightmap,
|
||||
};
|
||||
|
||||
struct GridUiPlan {
|
||||
GridUiOperation operation = GridUiOperation::request_heightmap_pick;
|
||||
std::string path;
|
||||
int texture_resolution = 0;
|
||||
int sample_count = 0;
|
||||
bool opens_picker = false;
|
||||
bool loads_heightmap = false;
|
||||
bool clears_heightmap = false;
|
||||
bool renders_lightmap = false;
|
||||
bool commits_heightmap = false;
|
||||
bool updates_preview = false;
|
||||
bool updates_ground_opacity = false;
|
||||
bool updates_shading_mode = false;
|
||||
bool shows_unsupported_message = false;
|
||||
bool shows_progress = false;
|
||||
bool mutates_grid_state = false;
|
||||
};
|
||||
|
||||
class GridUiServices {
|
||||
public:
|
||||
virtual ~GridUiServices() = default;
|
||||
|
||||
virtual void request_heightmap_pick() = 0;
|
||||
virtual pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) = 0;
|
||||
virtual void clear_heightmap(bool updates_preview) = 0;
|
||||
virtual void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) = 0;
|
||||
virtual void commit_heightmap(bool updates_ground_opacity) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
|
||||
{
|
||||
if (texture_resolution <= 0 || texture_resolution > 16384) {
|
||||
return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept
|
||||
{
|
||||
if (sample_count <= 0 || sample_count > 4096) {
|
||||
return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::request_heightmap_pick;
|
||||
plan.opens_picker = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("heightmap path must not be empty"));
|
||||
}
|
||||
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::load_heightmap;
|
||||
plan.path = std::string(path);
|
||||
plan.loads_heightmap = true;
|
||||
plan.updates_preview = true;
|
||||
plan.updates_ground_opacity = true;
|
||||
plan.mutates_grid_state = true;
|
||||
return pp::foundation::Result<GridUiPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::clear_heightmap;
|
||||
plan.clears_heightmap = true;
|
||||
plan.updates_preview = has_heightmap;
|
||||
plan.mutates_grid_state = has_heightmap;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
|
||||
{
|
||||
auto plan = plan_grid_heightmap_load(path);
|
||||
if (!plan) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
|
||||
}
|
||||
plan.value().operation = GridUiOperation::reload_heightmap;
|
||||
plan.value().updates_ground_opacity = false;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_lightmap_render(
|
||||
bool has_heightmap,
|
||||
bool supports_float32,
|
||||
bool supports_float16,
|
||||
int texture_resolution,
|
||||
int sample_count)
|
||||
{
|
||||
const auto texture_status = validate_grid_texture_resolution(texture_resolution);
|
||||
if (!texture_status.ok()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(texture_status);
|
||||
}
|
||||
|
||||
const auto sample_status = validate_grid_lightmap_samples(sample_count);
|
||||
if (!sample_status.ok()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(sample_status);
|
||||
}
|
||||
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::render_lightmap;
|
||||
plan.texture_resolution = texture_resolution;
|
||||
plan.sample_count = sample_count;
|
||||
if (!supports_float32 && !supports_float16) {
|
||||
plan.shows_unsupported_message = true;
|
||||
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.renders_lightmap = has_heightmap;
|
||||
plan.shows_progress = has_heightmap;
|
||||
plan.updates_shading_mode = has_heightmap;
|
||||
plan.mutates_grid_state = has_heightmap;
|
||||
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::commit_heightmap;
|
||||
plan.commits_heightmap = has_canvas;
|
||||
plan.updates_ground_opacity = has_canvas;
|
||||
plan.mutates_grid_state = has_canvas;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_grid_ui_plan(
|
||||
const GridUiPlan& plan,
|
||||
GridUiServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case GridUiOperation::request_heightmap_pick:
|
||||
if (!plan.opens_picker) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap pick plan must open a picker");
|
||||
}
|
||||
services.request_heightmap_pick();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case GridUiOperation::load_heightmap:
|
||||
case GridUiOperation::reload_heightmap:
|
||||
if (!plan.loads_heightmap || plan.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap load plan must provide a path");
|
||||
}
|
||||
return services.load_heightmap(plan.path, plan.updates_ground_opacity);
|
||||
|
||||
case GridUiOperation::clear_heightmap:
|
||||
if (!plan.clears_heightmap) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap clear plan must clear heightmap state");
|
||||
}
|
||||
services.clear_heightmap(plan.updates_preview);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case GridUiOperation::render_lightmap:
|
||||
{
|
||||
const auto texture_status = validate_grid_texture_resolution(plan.texture_resolution);
|
||||
if (!texture_status.ok()) {
|
||||
return texture_status;
|
||||
}
|
||||
const auto sample_status = validate_grid_lightmap_samples(plan.sample_count);
|
||||
if (!sample_status.ok()) {
|
||||
return sample_status;
|
||||
}
|
||||
if (!plan.shows_unsupported_message && !plan.renders_lightmap) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
services.render_lightmap(plan.shows_unsupported_message, plan.renders_lightmap);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
case GridUiOperation::commit_heightmap:
|
||||
if (plan.commits_heightmap) {
|
||||
services.commit_heightmap(plan.updates_ground_opacity);
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown grid UI operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
161
src/app_core/history_ui.h
Normal file
161
src/app_core/history_ui.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class HistoryUiOperation {
|
||||
undo,
|
||||
redo,
|
||||
clear,
|
||||
};
|
||||
|
||||
struct HistoryUiPlan {
|
||||
HistoryUiOperation operation = HistoryUiOperation::undo;
|
||||
int undo_count = 0;
|
||||
int redo_count = 0;
|
||||
int memory_bytes = 0;
|
||||
bool invokes_undo = false;
|
||||
bool invokes_redo = false;
|
||||
bool clears_history = false;
|
||||
bool updates_memory_label = false;
|
||||
bool updates_title = false;
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
class HistoryUiServices {
|
||||
public:
|
||||
virtual ~HistoryUiServices() = default;
|
||||
|
||||
virtual void invoke_undo() = 0;
|
||||
virtual void invoke_redo() = 0;
|
||||
virtual void clear_history() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
|
||||
{
|
||||
if (value < 0) {
|
||||
return pp::foundation::Status::out_of_range(message);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
|
||||
{
|
||||
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::undo;
|
||||
plan.undo_count = undo_count;
|
||||
if (undo_count == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.invokes_undo = true;
|
||||
plan.updates_memory_label = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
|
||||
{
|
||||
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::redo;
|
||||
plan.redo_count = redo_count;
|
||||
if (redo_count == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.invokes_redo = true;
|
||||
plan.updates_memory_label = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
|
||||
int undo_count,
|
||||
int redo_count,
|
||||
int memory_bytes)
|
||||
{
|
||||
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||
if (!undo_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
|
||||
}
|
||||
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||
if (!redo_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
|
||||
}
|
||||
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
|
||||
if (!memory_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.undo_count = undo_count;
|
||||
plan.redo_count = redo_count;
|
||||
plan.memory_bytes = memory_bytes;
|
||||
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.clears_history = true;
|
||||
plan.updates_memory_label = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
|
||||
const HistoryUiPlan& plan,
|
||||
HistoryUiServices& services)
|
||||
{
|
||||
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
|
||||
if (!undo_status.ok()) {
|
||||
return undo_status;
|
||||
}
|
||||
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
|
||||
if (!redo_status.ok()) {
|
||||
return redo_status;
|
||||
}
|
||||
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
|
||||
if (!memory_status.ok()) {
|
||||
return memory_status;
|
||||
}
|
||||
|
||||
if (plan.no_op) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
switch (plan.operation) {
|
||||
case HistoryUiOperation::undo:
|
||||
if (plan.invokes_undo) {
|
||||
services.invoke_undo();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
case HistoryUiOperation::redo:
|
||||
if (plan.invokes_redo) {
|
||||
services.invoke_redo();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
case HistoryUiOperation::clear:
|
||||
if (plan.clears_history) {
|
||||
services.clear_history();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown history operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
211
src/app_core/main_toolbar.h
Normal file
211
src/app_core/main_toolbar.h
Normal file
@@ -0,0 +1,211 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_canvas.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class MainToolbarCommand {
|
||||
open_document,
|
||||
save_document,
|
||||
undo,
|
||||
redo,
|
||||
clear_history,
|
||||
clear_canvas,
|
||||
show_message_box,
|
||||
show_settings,
|
||||
};
|
||||
|
||||
enum class MainToolbarAction {
|
||||
show_open_dialog,
|
||||
show_save_dialog,
|
||||
invoke_undo,
|
||||
invoke_redo,
|
||||
clear_history,
|
||||
clear_canvas,
|
||||
show_message_box,
|
||||
show_settings_dialog,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
struct MainToolbarPlan {
|
||||
MainToolbarCommand command = MainToolbarCommand::open_document;
|
||||
MainToolbarAction action = MainToolbarAction::show_open_dialog;
|
||||
std::string label;
|
||||
bool requires_canvas = false;
|
||||
bool updates_memory_label = false;
|
||||
bool updates_title = false;
|
||||
bool records_undo = false;
|
||||
bool marks_unsaved = false;
|
||||
bool no_op = false;
|
||||
HistoryUiPlan history;
|
||||
DocumentCanvasClearPlan canvas_clear;
|
||||
};
|
||||
|
||||
class MainToolbarServices {
|
||||
public:
|
||||
virtual ~MainToolbarServices() = default;
|
||||
|
||||
virtual void show_open_dialog() = 0;
|
||||
virtual void show_save_dialog() = 0;
|
||||
virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
|
||||
virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
|
||||
virtual void clear_history(const HistoryUiPlan& plan) = 0;
|
||||
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
|
||||
virtual void show_message_box() = 0;
|
||||
virtual void show_settings_dialog() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_main_toolbar_message_dialog()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Just a test message",
|
||||
"Longer description for the error or the message.",
|
||||
true);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<MainToolbarPlan> plan_main_toolbar_command(
|
||||
MainToolbarCommand command,
|
||||
int undo_count = 0,
|
||||
int redo_count = 0,
|
||||
int memory_bytes = 0,
|
||||
bool has_canvas = true)
|
||||
{
|
||||
MainToolbarPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case MainToolbarCommand::open_document:
|
||||
plan.action = MainToolbarAction::show_open_dialog;
|
||||
plan.label = "Open";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::save_document:
|
||||
plan.action = MainToolbarAction::show_save_dialog;
|
||||
plan.label = "Save";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::undo:
|
||||
{
|
||||
const auto history = plan_history_undo(undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().invokes_undo
|
||||
? MainToolbarAction::invoke_undo
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.updates_title = history.value().updates_title;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::redo:
|
||||
{
|
||||
const auto history = plan_history_redo(redo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().invokes_redo
|
||||
? MainToolbarAction::invoke_redo
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.updates_title = history.value().updates_title;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::clear_history:
|
||||
{
|
||||
const auto history = plan_history_clear(undo_count, redo_count, memory_bytes);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().clears_history
|
||||
? MainToolbarAction::clear_history
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::clear_canvas:
|
||||
{
|
||||
const auto clear = plan_document_canvas_clear(has_canvas);
|
||||
if (!clear) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(clear.status());
|
||||
}
|
||||
plan.action = clear.value().clears_canvas
|
||||
? MainToolbarAction::clear_canvas
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)";
|
||||
plan.requires_canvas = true;
|
||||
plan.records_undo = clear.value().records_undo;
|
||||
plan.marks_unsaved = clear.value().marks_unsaved;
|
||||
plan.no_op = clear.value().no_op;
|
||||
plan.canvas_clear = clear.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::show_message_box:
|
||||
plan.action = MainToolbarAction::show_message_box;
|
||||
plan.label = "Show Message Box";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::show_settings:
|
||||
plan.action = MainToolbarAction::show_settings_dialog;
|
||||
plan.label = "Settings";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown main toolbar command"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_main_toolbar_plan(
|
||||
const MainToolbarPlan& plan,
|
||||
MainToolbarServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case MainToolbarAction::show_open_dialog:
|
||||
services.show_open_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_save_dialog:
|
||||
services.show_save_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::invoke_undo:
|
||||
services.invoke_undo(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::invoke_redo:
|
||||
services.invoke_redo(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::clear_history:
|
||||
services.clear_history(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::clear_canvas:
|
||||
services.clear_canvas(plan.canvas_clear);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_message_box:
|
||||
services.show_message_box();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_settings_dialog:
|
||||
services.show_settings_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown main toolbar action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
302
src/app_core/quick_ui.h
Normal file
302
src/app_core/quick_ui.h
Normal file
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class QuickUiSlotKind {
|
||||
brush,
|
||||
color,
|
||||
};
|
||||
|
||||
enum class QuickUiOperation {
|
||||
select_slot,
|
||||
open_slot_popup,
|
||||
restore_state,
|
||||
reset_state,
|
||||
};
|
||||
|
||||
struct QuickUiPlan {
|
||||
QuickUiOperation operation = QuickUiOperation::select_slot;
|
||||
QuickUiSlotKind slot_kind = QuickUiSlotKind::brush;
|
||||
int slot_index = 0;
|
||||
int previous_index = 0;
|
||||
int brush_index = 0;
|
||||
int color_index = 0;
|
||||
int slot_count = 0;
|
||||
bool fire_event = false;
|
||||
bool updates_selection = false;
|
||||
bool opens_brush_popup = false;
|
||||
bool opens_color_picker = false;
|
||||
bool invokes_change_callback = false;
|
||||
bool restores_slots = false;
|
||||
bool resets_slots = false;
|
||||
bool redraws_brush_previews = false;
|
||||
bool mutates_quick_state = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewInput {
|
||||
bool ui_rtl = false;
|
||||
float slider_x = 0.0F;
|
||||
float slider_y = 0.0F;
|
||||
float slider_height = 0.0F;
|
||||
float zoom = 1.0F;
|
||||
bool has_pen_mode = false;
|
||||
bool has_line_mode = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewPlan {
|
||||
float cursor_x = 0.0F;
|
||||
float cursor_y = 0.0F;
|
||||
bool updates_pen_mode = false;
|
||||
bool updates_line_mode = false;
|
||||
bool draws_tip = false;
|
||||
bool disables_pen_outline = false;
|
||||
bool redraws_brush_preview = false;
|
||||
bool invokes_change_callback = false;
|
||||
};
|
||||
|
||||
class QuickUiServices {
|
||||
public:
|
||||
virtual ~QuickUiServices() = default;
|
||||
|
||||
virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0;
|
||||
virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0;
|
||||
virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0;
|
||||
virtual void reset_state(bool fire_event) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slider_float(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("quick slider preview value must be finite");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
|
||||
{
|
||||
if (slot_count <= 0) {
|
||||
return pp::foundation::Status::out_of_range("quick slot count must be greater than zero");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept
|
||||
{
|
||||
const auto count_status = validate_quick_slot_count(slot_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (slot_index < 0 || slot_index >= slot_count) {
|
||||
return pp::foundation::Status::out_of_range("quick slot index is out of range");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickSliderPreviewPlan> plan_quick_slider_preview(
|
||||
const QuickSliderPreviewInput& input)
|
||||
{
|
||||
const auto x_status = validate_quick_slider_float(input.slider_x);
|
||||
if (!x_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(x_status);
|
||||
}
|
||||
const auto y_status = validate_quick_slider_float(input.slider_y);
|
||||
if (!y_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(y_status);
|
||||
}
|
||||
const auto height_status = validate_quick_slider_float(input.slider_height);
|
||||
if (!height_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(height_status);
|
||||
}
|
||||
if (input.slider_height < 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview height must not be negative"));
|
||||
}
|
||||
const auto zoom_status = validate_quick_slider_float(input.zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(zoom_status);
|
||||
}
|
||||
if (input.zoom <= 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview zoom must be positive"));
|
||||
}
|
||||
|
||||
const float offset = input.ui_rtl ? -100.0F : 100.0F;
|
||||
QuickSliderPreviewPlan plan;
|
||||
plan.cursor_x = (input.slider_x + offset) * input.zoom;
|
||||
plan.cursor_y = (input.slider_y + input.slider_height * 0.5F) * input.zoom;
|
||||
plan.updates_pen_mode = input.has_pen_mode;
|
||||
plan.updates_line_mode = input.has_line_mode;
|
||||
plan.draws_tip = input.has_pen_mode || input.has_line_mode;
|
||||
plan.disables_pen_outline = input.has_pen_mode;
|
||||
plan.redraws_brush_preview = true;
|
||||
plan.invokes_change_callback = true;
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
|
||||
QuickUiSlotKind slot_kind,
|
||||
int current_index,
|
||||
int clicked_index,
|
||||
int slot_count)
|
||||
{
|
||||
const auto current_status = validate_quick_slot_index(current_index, slot_count);
|
||||
if (!current_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(current_status);
|
||||
}
|
||||
|
||||
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
|
||||
if (!clicked_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(clicked_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.slot_kind = slot_kind;
|
||||
plan.slot_index = clicked_index;
|
||||
plan.previous_index = current_index;
|
||||
plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0;
|
||||
plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0;
|
||||
plan.slot_count = slot_count;
|
||||
if (clicked_index != current_index) {
|
||||
plan.operation = QuickUiOperation::select_slot;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.operation = QuickUiOperation::open_slot_popup;
|
||||
plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush;
|
||||
plan.opens_color_picker = slot_kind == QuickUiSlotKind::color;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_restore(
|
||||
int brush_index,
|
||||
int color_index,
|
||||
int slot_count,
|
||||
bool fire_event)
|
||||
{
|
||||
const auto brush_status = validate_quick_slot_index(brush_index, slot_count);
|
||||
if (!brush_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(brush_status);
|
||||
}
|
||||
|
||||
const auto color_status = validate_quick_slot_index(color_index, slot_count);
|
||||
if (!color_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(color_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.operation = QuickUiOperation::restore_state;
|
||||
plan.brush_index = brush_index;
|
||||
plan.color_index = color_index;
|
||||
plan.slot_count = slot_count;
|
||||
plan.fire_event = fire_event;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = fire_event;
|
||||
plan.restores_slots = true;
|
||||
plan.redraws_brush_previews = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_reset(
|
||||
int slot_count,
|
||||
bool fire_event)
|
||||
{
|
||||
const auto count_status = validate_quick_slot_count(slot_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.operation = QuickUiOperation::reset_state;
|
||||
plan.brush_index = 0;
|
||||
plan.color_index = 0;
|
||||
plan.slot_count = slot_count;
|
||||
plan.fire_event = fire_event;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = fire_event;
|
||||
plan.resets_slots = true;
|
||||
plan.redraws_brush_previews = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan(
|
||||
const QuickUiPlan& plan,
|
||||
QuickUiServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case QuickUiOperation::select_slot:
|
||||
if (!plan.updates_selection) {
|
||||
return pp::foundation::Status::invalid_argument("quick select plan must update selection");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::open_slot_popup:
|
||||
if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) {
|
||||
return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup");
|
||||
}
|
||||
if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) {
|
||||
return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.open_slot_popup(plan.slot_kind, plan.slot_index);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::restore_state:
|
||||
if (!plan.restores_slots) {
|
||||
return pp::foundation::Status::invalid_argument("quick restore plan must restore slots");
|
||||
}
|
||||
{
|
||||
const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count);
|
||||
if (!brush_status.ok()) {
|
||||
return brush_status;
|
||||
}
|
||||
const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count);
|
||||
if (!color_status.ok()) {
|
||||
return color_status;
|
||||
}
|
||||
}
|
||||
services.restore_state(plan.brush_index, plan.color_index, plan.fire_event);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::reset_state:
|
||||
if (!plan.resets_slots) {
|
||||
return pp::foundation::Status::invalid_argument("quick reset plan must reset slots");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_count(plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.reset_state(plan.fire_event);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown quick UI operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
185
src/app_core/tools_menu.h
Normal file
185
src/app_core/tools_menu.h
Normal file
@@ -0,0 +1,185 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class ToolsMenuCommand {
|
||||
panels,
|
||||
options,
|
||||
clear_grids,
|
||||
reset_camera,
|
||||
shortcuts,
|
||||
sonarpen,
|
||||
};
|
||||
|
||||
enum class ToolsMenuAction {
|
||||
show_panels_submenu,
|
||||
show_options_submenu,
|
||||
clear_grid_overlays,
|
||||
reset_camera,
|
||||
show_shortcuts_dialog,
|
||||
start_sonarpen,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
enum class ToolsPanel {
|
||||
presets,
|
||||
color,
|
||||
color_advanced,
|
||||
layers,
|
||||
brush,
|
||||
grids,
|
||||
animation,
|
||||
};
|
||||
|
||||
enum class ToolsPanelAction {
|
||||
open_floating_panel,
|
||||
no_op_already_visible,
|
||||
};
|
||||
|
||||
struct ToolsMenuPlan {
|
||||
ToolsMenuCommand command = ToolsMenuCommand::panels;
|
||||
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
|
||||
std::string_view label;
|
||||
bool closes_root_popup = false;
|
||||
};
|
||||
|
||||
struct ToolsPanelPlan {
|
||||
ToolsPanel panel = ToolsPanel::presets;
|
||||
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
|
||||
std::string_view title;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int min_width = 0;
|
||||
int min_height = 0;
|
||||
bool droppable = true;
|
||||
bool hides_embedded_title = false;
|
||||
};
|
||||
|
||||
class ToolsMenuServices {
|
||||
public:
|
||||
virtual ~ToolsMenuServices() = default;
|
||||
|
||||
virtual void show_panels_submenu() = 0;
|
||||
virtual void show_options_submenu() = 0;
|
||||
virtual void clear_grid_overlays() = 0;
|
||||
virtual void reset_camera() = 0;
|
||||
virtual void show_shortcuts_dialog() = 0;
|
||||
virtual void start_sonarpen() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
|
||||
ToolsMenuCommand command,
|
||||
bool sonarpen_available = false) noexcept
|
||||
{
|
||||
switch (command) {
|
||||
case ToolsMenuCommand::panels:
|
||||
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
|
||||
case ToolsMenuCommand::options:
|
||||
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
|
||||
case ToolsMenuCommand::clear_grids:
|
||||
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
|
||||
case ToolsMenuCommand::reset_camera:
|
||||
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
|
||||
case ToolsMenuCommand::shortcuts:
|
||||
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
|
||||
case ToolsMenuCommand::sonarpen:
|
||||
return {
|
||||
command,
|
||||
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
|
||||
"SonarPen",
|
||||
sonarpen_available,
|
||||
};
|
||||
}
|
||||
|
||||
return { command, ToolsMenuAction::no_op_unavailable, "", false };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
|
||||
ToolsPanel panel,
|
||||
bool already_visible) noexcept
|
||||
{
|
||||
ToolsPanelPlan plan;
|
||||
plan.panel = panel;
|
||||
plan.action = already_visible
|
||||
? ToolsPanelAction::no_op_already_visible
|
||||
: ToolsPanelAction::open_floating_panel;
|
||||
|
||||
switch (panel) {
|
||||
case ToolsPanel::presets:
|
||||
plan.title = "Brushes";
|
||||
plan.height = 300;
|
||||
plan.min_height = 300;
|
||||
plan.min_width = 100;
|
||||
break;
|
||||
case ToolsPanel::color:
|
||||
plan.title = "Color Picker";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::color_advanced:
|
||||
plan.title = "Color Picker";
|
||||
plan.width = 300;
|
||||
plan.height = 300;
|
||||
break;
|
||||
case ToolsPanel::layers:
|
||||
plan.title = "Layers";
|
||||
plan.height = 300;
|
||||
plan.min_height = 100;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::brush:
|
||||
plan.title = "Brush Settings";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::grids:
|
||||
plan.title = "Grid";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::animation:
|
||||
plan.title = "Animation";
|
||||
plan.width = 500;
|
||||
plan.height = 300;
|
||||
plan.droppable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
|
||||
const ToolsMenuPlan& plan,
|
||||
ToolsMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case ToolsMenuAction::show_panels_submenu:
|
||||
services.show_panels_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::show_options_submenu:
|
||||
services.show_options_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::clear_grid_overlays:
|
||||
services.clear_grid_overlays();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::reset_camera:
|
||||
services.reset_camera();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::show_shortcuts_dialog:
|
||||
services.show_shortcuts_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::start_sonarpen:
|
||||
services.start_sonarpen();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown tools menu action");
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,520 +1,694 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/app_frame.h"
|
||||
#include "app_core/app_input.h"
|
||||
#include "app_core/document_platform_io.h"
|
||||
#include "app_core/document_sharing.h"
|
||||
#include "platform_api/platform_services.h"
|
||||
#include "platform_legacy/legacy_platform_services.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
void displayKeyboard(bool pShow);
|
||||
void android_pick_file(std::function<void(std::string)> callback);
|
||||
void android_pick_file_save(std::function<void(std::string)> callback);
|
||||
std::string android_get_clipboard();
|
||||
bool android_set_clipboard(const std::string& s);
|
||||
#elif _WIN32
|
||||
std::string win32_open_file(const char* filter);
|
||||
std::string win32_save_file(const char* filter);
|
||||
std::string win32_open_dir();
|
||||
void win32_show_cursor(bool visible);
|
||||
bool win32_clipboard_set_text(const std::string & s);
|
||||
std::string win32_clipboard_get_text();
|
||||
#elif __APPLE__
|
||||
#elif __LINUX__
|
||||
#include <tinyfiledialogs.h>
|
||||
#elif __WEB__
|
||||
void webgl_pick_file(std::function<void(std::string)> callback);
|
||||
void webgl_pick_file_save(const std::string& path,
|
||||
const std::string& name, std::function<void(bool)> callback);
|
||||
void webgl_sync();
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] GLint rgba8_internal_format() noexcept
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
|
||||
{
|
||||
const auto action = pp::app::plan_virtual_keyboard(visible);
|
||||
return visible
|
||||
? action == pp::app::VirtualKeyboardAction::show_keyboard
|
||||
: action == pp::app::VirtualKeyboardAction::hide_keyboard;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept
|
||||
{
|
||||
const auto action = pp::app::plan_cursor_visibility(visible);
|
||||
return visible
|
||||
? action == pp::app::CursorVisibilityAction::show_cursor
|
||||
: action == pp::app::CursorVisibilityAction::hide_cursor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
|
||||
{
|
||||
if (App::I)
|
||||
{
|
||||
if (auto* services = App::I->platform_services())
|
||||
return *services;
|
||||
}
|
||||
return pp::platform::legacy::platform_services();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
|
||||
{
|
||||
platform_services_ = services;
|
||||
}
|
||||
|
||||
pp::platform::PlatformServices* App::platform_services() const noexcept
|
||||
{
|
||||
return platform_services_;
|
||||
}
|
||||
|
||||
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
|
||||
{
|
||||
return active_platform_services().prepare_storage_paths();
|
||||
}
|
||||
|
||||
std::string App::clipboard_get_text()
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_get_text();
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_get_string];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_get_string];
|
||||
#elif __ANDROID__
|
||||
return android_get_clipboard();
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
|
||||
return {};
|
||||
|
||||
return active_platform_services().clipboard_text();
|
||||
}
|
||||
|
||||
bool App::clipboard_set_text(const std::string& s)
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_set_text(s);
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_set_string:s];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_set_string:s];
|
||||
#elif __ANDROID__
|
||||
return android_set_clipboard(s);
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
|
||||
return false;
|
||||
|
||||
return active_platform_services().set_clipboard_text(s);
|
||||
}
|
||||
|
||||
void App::stacktrace()
|
||||
{
|
||||
#if __OSX__
|
||||
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
|
||||
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
#endif
|
||||
active_platform_services().log_stacktrace();
|
||||
}
|
||||
|
||||
void App::crash_test()
|
||||
{
|
||||
#ifdef __IOS__
|
||||
[ios_view crash];
|
||||
#elif __OSX__
|
||||
[osx_view hockeyapp_crash];
|
||||
#elif defined(_WIN32)
|
||||
__debugbreak();
|
||||
#elif defined(__ANDROID__)
|
||||
int *x = nullptr; *x = 42;
|
||||
LOG("%d", *x);
|
||||
#endif
|
||||
active_platform_services().trigger_crash_test();
|
||||
}
|
||||
|
||||
void App::tick(float dt)
|
||||
{
|
||||
if (auto* main = layout_designer[main_id])
|
||||
const auto tick_plan = pp::app::plan_app_frame_tick(
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (auto* main = layout_designer[main_id]; tick_plan.tick_designer_layout && main)
|
||||
main->tick(dt);
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; tick_plan.tick_main_layout && main)
|
||||
main->tick(dt);
|
||||
}
|
||||
|
||||
void App::resize(float w, float h)
|
||||
{
|
||||
LOG("App::resize %d %d", (int)w, (int)h);
|
||||
uirtt.create(w, h, -1, GL_RGBA8, true);
|
||||
redraw = true;
|
||||
width = w;
|
||||
height = h;
|
||||
const auto resize_plan = pp::app::plan_app_resize(w, h);
|
||||
if (!resize_plan) {
|
||||
LOG("App resize plan failed: %s", resize_plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resize_plan.value().recreate_ui_render_target) {
|
||||
uirtt.create(
|
||||
resize_plan.value().render_target_width,
|
||||
resize_plan.value().render_target_height,
|
||||
-1,
|
||||
rgba8_internal_format(),
|
||||
true);
|
||||
}
|
||||
redraw = resize_plan.value().request_redraw;
|
||||
width = resize_plan.value().width;
|
||||
height = resize_plan.value().height;
|
||||
}
|
||||
|
||||
void App::show_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(true);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:true];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(true);
|
||||
}
|
||||
|
||||
void App::hide_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(false);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:false];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(false);
|
||||
}
|
||||
|
||||
void App::showKeyboard()
|
||||
{
|
||||
LOG("show keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view show_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(true);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(true);
|
||||
}
|
||||
|
||||
void App::hideKeyboard()
|
||||
{
|
||||
LOG("hide keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view hide_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(false);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(false);
|
||||
}
|
||||
|
||||
void App::pick_image(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view pick_photo:callback];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
|
||||
std::string path = [osx_view pick_file:fileTypes];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif _WIN32
|
||||
std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
callback(p);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#endif
|
||||
active_platform_services().pick_image(std::move(callback));
|
||||
}
|
||||
|
||||
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
||||
for (const auto& t : types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
[ios_view pick_file:fileTypes then:callback];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
||||
for (const auto& t : types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
std::string path = [osx_view pick_file:fileTypes];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif _WIN32
|
||||
std::string filter = "Supported Files (";
|
||||
bool first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.append(")");
|
||||
filter.push_back(0);
|
||||
first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.push_back(0);
|
||||
std::string path = win32_open_file(filter.c_str());
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
callback(p);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#endif
|
||||
active_platform_services().pick_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
#if __IOS__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
std::string ext = "." + type;
|
||||
std::string path = tmp_path + "/" + default_name + ext;
|
||||
std::thread([=]{
|
||||
writer(path);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view pick_file_save:path];
|
||||
});
|
||||
callback(path, true);
|
||||
}).detach();
|
||||
const auto target = active_platform_services().prepare_writable_file(type, default_name, data_path, tmp_path);
|
||||
if (target.path.empty())
|
||||
{
|
||||
callback({}, false);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("App::pick_file_save %s", target.path.c_str());
|
||||
auto write_and_save = [=] {
|
||||
writer(target.path);
|
||||
save_prepared_file(target.path, target.suggested_name, callback);
|
||||
};
|
||||
|
||||
if (target.write_on_background_thread)
|
||||
std::thread(write_and_save).detach();
|
||||
else
|
||||
write_and_save();
|
||||
}
|
||||
#elif __WEB__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
auto path = data_path + "/" + default_name + "." + type;
|
||||
LOG("App::pick_file_save %s", path.c_str());
|
||||
writer(path);
|
||||
webgl_pick_file_save(path, default_name + "." + type, callback);
|
||||
}
|
||||
#else
|
||||
|
||||
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#if __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
//NSArray* fileTypes = [NSArray arrayWithObjects:@"ppi", @"PPI", nil];
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
||||
for (const auto& t : types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
std::string path = [osx_view pick_file_save:fileTypes];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file_save(callback);
|
||||
#elif _WIN32
|
||||
std::string filter = "Supported Files (";
|
||||
bool first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.append(")");
|
||||
filter.push_back(0);
|
||||
first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.push_back(0);
|
||||
std::string path = win32_save_file(filter.c_str());
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#endif
|
||||
active_platform_services().pick_save_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
bool App::uses_prepared_file_writes() const
|
||||
{
|
||||
return active_platform_services().uses_prepared_file_writes();
|
||||
}
|
||||
|
||||
bool App::uses_work_directory_document_export_collections() const
|
||||
{
|
||||
return active_platform_services().uses_work_directory_document_export_collections();
|
||||
}
|
||||
|
||||
bool App::disables_network_tls_verification() const
|
||||
{
|
||||
return active_platform_services().disables_network_tls_verification();
|
||||
}
|
||||
|
||||
bool App::uses_ppbr_export_data_directory_override() const
|
||||
{
|
||||
return active_platform_services().uses_ppbr_export_data_directory_override();
|
||||
}
|
||||
|
||||
bool App::platform_supports_sonarpen() const
|
||||
{
|
||||
return active_platform_services().supports_sonarpen();
|
||||
}
|
||||
|
||||
void App::start_platform_sonarpen()
|
||||
{
|
||||
active_platform_services().start_sonarpen();
|
||||
}
|
||||
|
||||
int App::default_canvas_resolution() const
|
||||
{
|
||||
return active_platform_services().default_canvas_resolution();
|
||||
}
|
||||
|
||||
bool App::draws_canvas_tip_for_input(kEventSource source, kEventType type) const
|
||||
{
|
||||
return active_platform_services().draws_canvas_tip_for_pointer(
|
||||
source == kEventSource::Mouse,
|
||||
source == kEventSource::Stylus,
|
||||
type == kEventType::MouseUpL);
|
||||
}
|
||||
|
||||
float App::adjust_canvas_input_pressure(float pressure) const
|
||||
{
|
||||
return active_platform_services().adjust_canvas_input_pressure(pressure);
|
||||
}
|
||||
#endif
|
||||
|
||||
void App::pick_dir(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
// NOT IMPLEMENTED
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
std::string path = [osx_view pick_dir];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
// NOT IMPLEMENTED
|
||||
#elif _WIN32
|
||||
// TODO: to be implemented
|
||||
std::string path = win32_open_dir();
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#endif
|
||||
active_platform_services().pick_directory(std::move(callback));
|
||||
}
|
||||
|
||||
bool App::supports_working_directory_picker() const
|
||||
{
|
||||
return active_platform_services().supports_working_directory_picker();
|
||||
}
|
||||
|
||||
std::string App::format_working_directory_path(std::string_view path) const
|
||||
{
|
||||
return active_platform_services().format_working_directory_path(path);
|
||||
}
|
||||
|
||||
void App::display_file(std::string path)
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view display_file:path];
|
||||
});
|
||||
#elif __OSX__
|
||||
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:path.c_str()]];
|
||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// std::string path = [osx_view pick_file];
|
||||
// if (!path.empty())
|
||||
// callback(path);
|
||||
// });
|
||||
#elif __ANDROID__
|
||||
//displayKeyboard(and_app, false);
|
||||
#elif _WIN32
|
||||
// std::string path = win32_open_file();
|
||||
// if (!path.empty())
|
||||
// callback(path);
|
||||
#endif
|
||||
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
|
||||
return;
|
||||
|
||||
active_platform_services().display_file(path);
|
||||
}
|
||||
|
||||
void App::share_file(std::string path)
|
||||
{
|
||||
if (path.empty())
|
||||
const auto plan = pp::app::plan_document_share(path);
|
||||
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
|
||||
{
|
||||
message_box("Sharing failed", "Please save the document before sharing it.");
|
||||
return;
|
||||
}
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
#elif _WIN32
|
||||
// not implemented
|
||||
#endif
|
||||
active_platform_services().share_file(path);
|
||||
}
|
||||
|
||||
void App::request_app_close()
|
||||
{
|
||||
active_platform_services().request_app_close();
|
||||
}
|
||||
|
||||
bool App::start_platform_vr_mode()
|
||||
{
|
||||
return active_platform_services().start_vr_mode();
|
||||
}
|
||||
|
||||
void App::stop_platform_vr_mode()
|
||||
{
|
||||
active_platform_services().stop_vr_mode();
|
||||
}
|
||||
|
||||
void App::attach_ui_thread()
|
||||
{
|
||||
active_platform_services().attach_ui_thread();
|
||||
}
|
||||
|
||||
void App::detach_ui_thread()
|
||||
{
|
||||
active_platform_services().detach_ui_thread();
|
||||
}
|
||||
|
||||
void App::acquire_render_context()
|
||||
{
|
||||
active_platform_services().acquire_render_context();
|
||||
}
|
||||
|
||||
void App::release_render_context()
|
||||
{
|
||||
active_platform_services().release_render_context();
|
||||
}
|
||||
|
||||
void App::present_render_context()
|
||||
{
|
||||
active_platform_services().present_render_context();
|
||||
}
|
||||
|
||||
void App::bind_default_render_target()
|
||||
{
|
||||
active_platform_services().bind_default_render_target();
|
||||
}
|
||||
|
||||
void App::bind_main_render_target()
|
||||
{
|
||||
active_platform_services().bind_main_render_target();
|
||||
}
|
||||
|
||||
void App::apply_render_platform_hints()
|
||||
{
|
||||
active_platform_services().apply_render_platform_hints();
|
||||
}
|
||||
|
||||
void App::install_render_debug_callback()
|
||||
{
|
||||
active_platform_services().install_render_debug_callback();
|
||||
}
|
||||
|
||||
void App::begin_render_capture_frame()
|
||||
{
|
||||
active_platform_services().begin_render_capture_frame();
|
||||
}
|
||||
|
||||
void App::end_render_capture_frame()
|
||||
{
|
||||
active_platform_services().end_render_capture_frame();
|
||||
}
|
||||
|
||||
bool App::platform_deletes_recorded_files_on_clear()
|
||||
{
|
||||
return active_platform_services().deletes_recorded_files_on_clear();
|
||||
}
|
||||
|
||||
void App::clear_platform_recorded_files(std::string path)
|
||||
{
|
||||
active_platform_services().clear_recorded_files(path);
|
||||
}
|
||||
|
||||
void App::publish_exported_image(std::string path)
|
||||
{
|
||||
active_platform_services().publish_exported_image(path);
|
||||
}
|
||||
|
||||
void App::flush_platform_storage()
|
||||
{
|
||||
active_platform_services().flush_persistent_storage();
|
||||
}
|
||||
|
||||
std::vector<std::string> App::document_browse_roots() const
|
||||
{
|
||||
return active_platform_services().document_browse_roots(work_path, data_path);
|
||||
}
|
||||
|
||||
void App::save_platform_ui_state()
|
||||
{
|
||||
active_platform_services().save_ui_state();
|
||||
}
|
||||
|
||||
bool App::platform_enables_live_asset_reloading()
|
||||
{
|
||||
return active_platform_services().enables_live_asset_reloading();
|
||||
}
|
||||
|
||||
void App::update_platform_frame(float delta_time_seconds)
|
||||
{
|
||||
active_platform_services().update_platform_frame(delta_time_seconds);
|
||||
}
|
||||
|
||||
void App::report_rendered_frames(int frames)
|
||||
{
|
||||
active_platform_services().report_rendered_frames(frames);
|
||||
}
|
||||
|
||||
void App::save_prepared_file(
|
||||
std::string path,
|
||||
std::string suggested_name,
|
||||
std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
active_platform_services().save_prepared_file(
|
||||
path,
|
||||
suggested_name,
|
||||
[callback = std::move(callback)](std::string saved_path, bool saved) {
|
||||
callback(saved_path, saved);
|
||||
});
|
||||
}
|
||||
|
||||
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse down dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseDownR : kEventType::MouseDownL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_move(float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
cursor = { x / zoom, y / zoom };
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
cursor = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseMove;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_up(int button, float x, float y, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse up dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseUpR : kEventType::MouseUpL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_scroll(float x, float y, float delta)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse scroll dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseScroll;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_scroll_delta = delta;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_cancel(int button)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_mouse_cancel_dispatch(
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseCancel;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_start(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture start dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureStart;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
gesture_p0 = p0;
|
||||
gesture_p1 = p1;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_move(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
gesture_p0.x,
|
||||
gesture_p0.y,
|
||||
gesture_p1.x,
|
||||
gesture_p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureMove;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_distance_delta = e.m_distance - glm::distance(gesture_p0, gesture_p1);
|
||||
e.m_pos_delta = p - glm::lerp(gesture_p0, gesture_p1, 0.5f);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
e.m_distance_delta = plan.value().distance_delta;
|
||||
e.m_pos_delta = { plan.value().position_delta_x, plan.value().position_delta_y };
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_end()
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
GestureEvent e;
|
||||
e.m_type = kEventType::GestureEnd;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::touch_tap(const glm::vec2& pos, int fingers, int tap_count)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
TouchEvent e;
|
||||
e.m_type = kEventType::TouchTap;
|
||||
e.m_finger_count = fingers;
|
||||
e.m_tap_count = tap_count;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_down(kKey key)
|
||||
{
|
||||
if (key == kKey::KeySpacebar && vr_active)
|
||||
const auto plan = pp::app::plan_app_key_down_dispatch(
|
||||
layout.get(main_id) != nullptr,
|
||||
key == kKey::KeySpacebar,
|
||||
vr_active);
|
||||
if (plan.sync_vr_camera_rotation)
|
||||
canvas->m_canvas->m_cam_rot = vr_rot;
|
||||
redraw = true;
|
||||
keys[(int)key] = true;
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyDown;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_up(kKey key)
|
||||
{
|
||||
redraw = true;
|
||||
keys[(int)key] = false;
|
||||
const auto plan = pp::app::plan_app_key_up_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyUp;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_char(char key)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyChar;
|
||||
e.m_char = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
|
||||
void App::toggle_ui()
|
||||
{
|
||||
auto m = layout[main_id]->m_children[1];
|
||||
ui_visible = !ui_visible;
|
||||
for (int i = 1; i < m->m_children.size(); i++)
|
||||
m->m_children[i]->m_display = ui_visible;
|
||||
auto* main = layout[main_id];
|
||||
const std::size_t main_child_count = main ? main->m_children.size() : 0U;
|
||||
auto* panel_container = main_child_count > 1U ? main->m_children[1].get() : nullptr;
|
||||
const auto plan = pp::app::plan_app_ui_visibility_toggle(
|
||||
ui_visible,
|
||||
main != nullptr,
|
||||
main_child_count,
|
||||
panel_container ? panel_container->m_children.size() : 0U);
|
||||
if (!plan) {
|
||||
LOG("UI toggle plan failed: %s", plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_visible = plan.value().next_ui_visible;
|
||||
if (!panel_container)
|
||||
return;
|
||||
|
||||
for (std::size_t i = plan.value().first_panel_child_index;
|
||||
i < plan.value().panel_child_count;
|
||||
++i) {
|
||||
panel_container->m_children[i]->m_display = ui_visible;
|
||||
}
|
||||
}
|
||||
|
||||
void App::set_stylus()
|
||||
{
|
||||
has_stylus = true;
|
||||
if (canvas)
|
||||
const auto plan = pp::app::plan_app_stylus_attach(canvas != nullptr);
|
||||
has_stylus = plan.set_has_stylus;
|
||||
if (plan.enable_canvas_touch_lock && canvas)
|
||||
canvas->m_canvas->m_touch_lock = true;
|
||||
}
|
||||
|
||||
1429
src/app_layout.cpp
1429
src/app_layout.cpp
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user