Compare commits
1139 Commits
main
...
codex/mode
| Author | SHA1 | Date | |
|---|---|---|---|
| d2a841f348 | |||
| 18665bdffc | |||
| d135835787 | |||
| 8afeb087b8 | |||
| 551fe6c94a | |||
| 6a9c415d85 | |||
| 4a5bb68fe2 | |||
| d68c97e609 | |||
| bde9f0c4f2 | |||
| 52d633c6e1 | |||
| ad76aeb751 | |||
| 667589f1f6 | |||
| d5b137c9ff | |||
| 52f0d32612 | |||
| 3e4eb89499 | |||
| 640ebc4be4 | |||
| 76ca2eea1a | |||
| 2948e907bc | |||
| 34e2747867 | |||
| 73c13f8cde | |||
| 7ef399eb75 | |||
| 0c72aa0312 | |||
| 6f4bd4b26f | |||
| 17b603536b | |||
| 3366b54c7f | |||
| 4d7a23a1fd | |||
| 75f57213ca | |||
| 953fa11744 | |||
| 56c4743e66 | |||
| a76560e3df | |||
| c3d757f4a4 | |||
| 8fed697643 | |||
| 5392b7a0aa | |||
| 4e2230681a | |||
| 57128ce7d3 | |||
| a5691e24c6 | |||
| fac4b4245c | |||
| 4db4f7e226 | |||
| 162871ee81 | |||
| 01abfdfc83 | |||
| f744e25640 | |||
| 4653197083 | |||
| d3e875e053 | |||
| c628310d6d | |||
| ded2bf3464 | |||
| ce0ced70c6 | |||
| 1e9b93eaab | |||
| cebb9b998e | |||
| 8170ff1590 | |||
| abfe745221 | |||
| e796128b7a | |||
| e9d72cc532 | |||
| b36dec3460 | |||
| 82ce26c476 | |||
| 5ca13c1584 | |||
| 20038bbf8f | |||
| 7e1c2ee502 | |||
| 6b8100ac82 | |||
| 8f3a5574c5 | |||
| 6a05026543 | |||
| 0060e810fd | |||
| 249753f887 | |||
| 36fcf5fbab | |||
| f1a9904fc1 | |||
| cfe4d554f0 | |||
| b8b8acee98 | |||
| b9b2fe9a0d | |||
| 9f792ca85f | |||
| e906f355d5 | |||
| 835df8284a | |||
| 49733e4754 | |||
| 5e25bec2d7 | |||
| 0c695b5e2b | |||
| 7a06c0f728 | |||
| 4e4c5b6b45 | |||
| acb77167df | |||
| d265d0045b | |||
| 6b9a8958b2 | |||
| c88a50b494 | |||
| 938b5bebdf | |||
| 7bab6e4c2e | |||
| de3d284b66 | |||
| d1c4ddd5e1 | |||
| 76d08999ab | |||
| 95c6235667 | |||
| b534537349 | |||
| d75813b145 | |||
| 72f2ba1ca6 | |||
| 7259fd5acf | |||
| fa1b4f8d9f | |||
| 904c8e644b | |||
| c46559347a | |||
| 4740aac698 | |||
| 79d516312b | |||
| af85e1cfe1 | |||
| 6b09fb1e4c | |||
| 97fa863e62 | |||
| b51d2f8163 | |||
| cf9e22c912 | |||
| 82157dbbb2 | |||
| fc878c8327 | |||
| 01a9cf8d7e | |||
| 6e44dad49f | |||
| bc93d67d10 | |||
| 6e8fa41092 | |||
| c3458fca6f | |||
| fd3ef75aff | |||
| ef40a7909b | |||
| 077deb7ae2 | |||
| ca19997690 | |||
| 9b2bda1b29 | |||
| a89aa9fd25 | |||
| 861dd82108 | |||
| 46ac1e8ee4 | |||
| 19d293c699 | |||
| 8a0f1e867d | |||
| 9872820392 | |||
| 85cefacabc | |||
| 9445d86793 | |||
| 11f898c887 | |||
| d37c91ff57 | |||
| 2721bbf5e6 | |||
| 74efaa6976 | |||
| 9a8c2d1393 | |||
| 229813fba6 | |||
| 260e714c47 | |||
| 33d6b1489a | |||
| 96d9dd0091 | |||
| ed16198397 | |||
| 1f418dcd42 | |||
| 9d7c338dab | |||
| a04c98213c | |||
| a181e9d033 | |||
| 2c63070e8b | |||
| 82f2b5968d | |||
| 03c0068ad2 | |||
| 145a147696 | |||
| 3cd5e99d0c | |||
| c3a2a14186 | |||
| a96c4f8643 | |||
| ecf83db458 | |||
| ffbd66ea77 | |||
| 897e3b09f3 | |||
| a943a2aa7d | |||
| cef20e5ca1 | |||
| 81e86d8f64 | |||
| af20924ef1 | |||
| 5ecee1ace2 | |||
| 70dee93abe | |||
| ca2796affc | |||
| 8c93e2477c | |||
| f43ac90ddf | |||
| a3884d730b | |||
| 7366880595 | |||
| cefa8e7774 | |||
| 29a7040930 | |||
| 8ad9dbad73 | |||
| 395ca52065 | |||
| 4ecf3d84a7 | |||
| c93904b17c | |||
| 0bbe0f26be | |||
| 4e1254ec49 | |||
| 315740978f | |||
| 729beeb92f | |||
| 3a65495f5f | |||
| df2d0a16c1 | |||
| 03df926ab7 | |||
| 7ee7b76a3d | |||
| 6eff6f7e7a | |||
| 2ae7411ba6 | |||
| 80d8c078f6 | |||
| 02f38535df | |||
| 845a26a74f | |||
| 47b63c0e67 | |||
| b994ee7605 | |||
| 0f9c20264e | |||
| 729c2e3657 | |||
| fb7fab24f4 | |||
| edd672d1a3 | |||
| 6d7435b555 | |||
| 3202c959ba | |||
| abe1567413 | |||
| 55a79ee436 | |||
| 12cd9188e9 | |||
| 360b1aa46b | |||
| 1b97119847 | |||
| 846d4d7b95 | |||
| 1acd3e4d09 | |||
| 2a585f0058 | |||
| eacf862c0b | |||
| 530e572e3a | |||
| 9bf79beec2 | |||
| 6844256937 | |||
| c2083565bf | |||
| de2aedb96b | |||
| 2f2b96211d | |||
| f88b2094ef | |||
| 7b8da2f0e2 | |||
| a401ab356b | |||
| 39728e463a | |||
| 459ace29e3 | |||
| 294d9ce74f | |||
| 3e1b1890a2 | |||
| ffda49ad0e | |||
| 78f0ea9bd3 | |||
| 98c48c33da | |||
| d85d702434 | |||
| aaf55dd797 | |||
| 02552c9a12 | |||
| be8dee8de5 | |||
| f6afd34256 | |||
| c37451e959 | |||
| eaa8a2fced | |||
| 9384676367 | |||
| 21b529aac5 | |||
| 16111e09b1 | |||
| b1cd0887f4 | |||
| 359e6b949e | |||
| 8c0b89af07 | |||
| 69603ed6c9 | |||
| 143d21b433 | |||
| b5317832f6 | |||
| b93f31bc32 | |||
| 9e731b4a71 | |||
| 56e0db2522 | |||
| 07f3ca81f0 | |||
| 91c3d2b2d8 | |||
| 7850d90efe | |||
| 8722820224 | |||
| 2b8c11bfb2 | |||
| c8bd8c3668 | |||
| 65d762c699 | |||
| a78f444771 | |||
| e993fa4896 | |||
| 8ecf04dce5 | |||
| 88018f6c69 | |||
| f3abc1354f | |||
| 2e520709dd | |||
| bed95cc059 | |||
| 777579fffe | |||
| 58f0229d8f | |||
| 4c6b39c21a | |||
| 12244b2e0a | |||
| 3d5340a380 | |||
| 16ba3e42fd | |||
| df2c67838b | |||
| 0cec8e1bfd | |||
| 2ccd83440a | |||
| 58e0d1cc69 | |||
| a3c7af716d | |||
| 87e1307d12 | |||
| 34e9789f1f | |||
| 56c24db891 | |||
| 54b6aee2e4 | |||
| 740ad37709 | |||
| 9b6fe73af2 | |||
| abd72a790d | |||
| 45f08ee8e4 | |||
| 0ba7eb8331 | |||
| b06211faa0 | |||
| 9d54dbc8c7 | |||
| e9db32f274 | |||
| 217702b6be | |||
| 80533fae5b | |||
| fdf2e56182 | |||
| 3e0a6b2c31 | |||
| 02e6386664 | |||
| 58ff301580 | |||
| d2418b824d | |||
| 9efb080202 | |||
| 63f0cd1773 | |||
| ccde4d69f4 | |||
| 1a5868376b | |||
| 5bf0a4f61b | |||
| ec71575b5d | |||
| 8db859cb2c | |||
| 54fbf900fc | |||
| 33ff4b9b93 | |||
| 328a793dd2 | |||
| ce169a3fd6 | |||
| 168404433c | |||
| e1e686d3f7 | |||
| 565564c061 | |||
| f907d88c26 | |||
| f78fc3076c | |||
| 68617e8bc4 | |||
| 62d4c16e72 | |||
| e5f2255b08 | |||
| c9fb9fa986 | |||
| 6b628abfc0 | |||
| be42224561 | |||
| 8f02e39058 | |||
| 05386598fc | |||
| f4cd08d700 | |||
| 7baf377c44 | |||
| cd9b517bfd | |||
| d5af0b984e | |||
| ed2c683ced | |||
| e98c8f4840 | |||
| 90334a0317 | |||
| c21adfda0e | |||
| c01c5b5f8c | |||
| 1f7c30f183 | |||
| e8fe66da10 | |||
| 4c164f4f73 | |||
| f3a48fbc69 | |||
| 8e1aea9a2d | |||
| b9a997a80f | |||
| 764f20084a | |||
| 27d34f2fba | |||
| 61a3ee5f34 | |||
| f3f694e1e7 | |||
| 0f5721f066 | |||
| b10b2788a3 | |||
| 1a28716e94 | |||
| 91bd37bca5 | |||
| 85f8af42d1 | |||
| 1e4a6814be | |||
| d59d130b7f | |||
| a16ac39d67 | |||
| b05049455a | |||
| 04654e377e | |||
| d46399f44a | |||
| c87a304e72 | |||
| 5d5e0e7f21 | |||
| 538441a5dc | |||
| ce075a40d6 | |||
| c147c1d163 | |||
| aa213a69f8 | |||
| 5f66d0e76e | |||
| cc1dd0dd95 | |||
| 6670f6e186 | |||
| 2053c55bd6 | |||
| 3cd0bdb026 | |||
| cf859cd4c2 | |||
| 4484880e32 | |||
| aa53a5f9ac | |||
| a860c74f60 | |||
| 7cafaaa1a6 | |||
| 3e7a9d5cab | |||
| 3672f9a514 | |||
| a2e805f991 | |||
| b9647847f0 | |||
| dbf4db594a | |||
| 3c3405d796 | |||
| ffb653fc6e | |||
| ca1ba4f1ce | |||
| 3acb2da300 | |||
| 3d8f798412 | |||
| 8f83145892 | |||
| 666c4dd308 | |||
| e00f513126 | |||
| 69515c497b | |||
| 83a4677088 | |||
| 683a41f1dc | |||
| 73c48d9d31 | |||
| 91d4da0910 | |||
| 618bb89517 | |||
| e507fe2786 | |||
| 6a9439a804 | |||
| 3478219a3e | |||
| c4c7994f88 | |||
| e5e334bf74 | |||
| 4661305733 | |||
| 51c70c9ecc | |||
| 07b188de4a | |||
| a99a324e5e | |||
| e6f3be1c2e | |||
| fddddbb76c | |||
| fa002097eb | |||
| 4bca83982c | |||
| 59a83468a6 | |||
| 2118693c1e | |||
| 2ed469cdc8 | |||
| d03f0c6371 | |||
| 884357e18e | |||
| 7f99be6eee | |||
| cef140842a | |||
| 1d3d524d3b | |||
| 466470db6c | |||
| 7e8be835f5 | |||
| 74ecf11d64 | |||
| 89e3d75a09 | |||
| 065ddf8ebc | |||
| 92c830ed64 | |||
| 5502eec086 | |||
| d12b57974b | |||
| 5c1cfa2b0e | |||
| 2818d9c4e5 | |||
| 87e6c1118b | |||
| bbcaac708f | |||
| d5c7c8c6ae | |||
| e7d96bfdc4 | |||
| 56d459623d | |||
| 19affeab87 | |||
| 037be1a72a | |||
| c118b92b86 | |||
| e8fdd96d37 | |||
| 42d4f6df1b | |||
| b9ed78e1b5 | |||
| 49caa42379 | |||
| 60d5a6aab9 | |||
| 2e1e6e25c7 | |||
| 9ed24d1667 | |||
| 161900c517 | |||
| 29f447293a | |||
| e135e5abdc | |||
| f122efbad0 | |||
| c542fd0083 | |||
| 536f268349 | |||
| 532286e81a | |||
| 353dfb4e26 | |||
| 9d8495fa03 | |||
| 6220f333b9 | |||
| 0c5522f272 | |||
| 341d114d99 | |||
| b9f9ecaa99 | |||
| 0abd355910 | |||
| 4229f17f1a | |||
| 2887d02484 | |||
| ecc3b3edad | |||
| eb60c23e91 | |||
| f53f943374 | |||
| b1a1bc07a4 | |||
| 3d999225f3 | |||
| 3f4d3b38b0 | |||
| 2ec4896578 | |||
| 2daec76f02 | |||
| 623fdc6718 | |||
| 01534ef21c | |||
| fc4f5e401d | |||
| 936081680f | |||
| 1d50bcc741 | |||
| ca7ea820ba | |||
| f05e4144b4 | |||
| a6855fca05 | |||
| a428f77db6 | |||
| fc4fbb7954 | |||
| 55fb02e472 | |||
| defb1af0d9 | |||
| c62a60a4af | |||
| 5b76f3d1f9 | |||
| beae99fb9c | |||
| 6ba98eea70 | |||
| 3c6fd00ae3 | |||
| 7e09298efe | |||
| 748bec9486 | |||
| dd68c5de89 | |||
| 954531743a | |||
| d441e5e2bc | |||
| 1a5d828d5c | |||
| bef1482821 | |||
| 718c9224b9 | |||
| b5b7bcc3cf | |||
| 6b12c520f0 | |||
| 819b0f31db | |||
| 58885187ba | |||
| 93a5d1ac07 | |||
| 96b7b6f870 | |||
| 4534e0ec6d | |||
| f8c5efdddb | |||
| 4a44e6cd9e | |||
| dd9d9f532c | |||
| 87c4bee112 | |||
| 3d3a99a536 | |||
| 3a7151ae7f | |||
| 547a660412 | |||
| fa6ac4dcf9 | |||
| fe16a6a270 | |||
| 86777b26b5 | |||
| 42bc1866ad | |||
| e7813c2ff0 | |||
| e01c88921a | |||
| e872e40467 | |||
| 11a62e9b43 | |||
| 9bbc24b075 | |||
| fee09e5340 | |||
| 9acd3fa524 | |||
| ae46be9f90 | |||
| 779694ae1b | |||
| ded4573216 | |||
| 4c9809f7fc | |||
| f4176aa234 | |||
| 07a14fdd56 | |||
| 6bb1268edb | |||
| 38a73fc6f0 | |||
| 5c03b13078 | |||
| 384db00015 | |||
| f3364a96ae | |||
| 0aa0ac4497 | |||
| 818f2b10ad | |||
| f0cc49396e | |||
| 7659f4907b | |||
| 2a29ebb1a9 | |||
| 5891d2839d | |||
| 43bdc85c11 | |||
| 3986bd3c70 | |||
| c51f79eee3 | |||
| 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/
|
||||
|
||||
175
AGENTS.md
Normal file
175
AGENTS.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 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 coordinator/worker
|
||||
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.
|
||||
- Treat automatic compaction as a failure mode to avoid. Keep active context
|
||||
small, commit and push before the thread grows large, and reset conversation
|
||||
context between verified slices when practical instead of carrying excess
|
||||
history forward. 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
|
||||
coordinator keeps integration locally, assigns direct worker tasks, uses
|
||||
`gpt-5.4-mini` workers by default, and gives them a minimal task packet with
|
||||
only the build, test, and code-exploration context needed so they do not
|
||||
spend tokens re-reading repo docs.
|
||||
|
||||
## 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.
|
||||
657
CMakeLists.txt
Normal file
657
CMakeLists.txt
Normal file
@@ -0,0 +1,657 @@
|
||||
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
|
||||
${PP_PLATFORM_LINUX_SOURCES})
|
||||
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_ui_core
|
||||
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()
|
||||
309
CMakePresets.json
Normal file
309
CMakePresets.json
Normal file
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"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": "renderer-conformance",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "renderer-conformance"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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)
|
||||
96
cmake/PanoPainterPackageTargets.cmake
Normal file
96
cmake/PanoPainterPackageTargets.cmake
Normal file
@@ -0,0 +1,96 @@
|
||||
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_windows_appx_package_readiness
|
||||
COMMENT "Report Windows AppX package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds windows-appx)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_apple_bundle_package_readiness
|
||||
COMMENT "Report Apple bundle package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds apple-bundle)
|
||||
|
||||
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_standard_apk_package_readiness
|
||||
COMMENT "Report Android standard APK package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-AndroidNativeChecks
|
||||
-PackageKinds android-standard-apk)
|
||||
|
||||
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_quest_apk_package_readiness
|
||||
COMMENT "Report Android Quest APK package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-AndroidNativeChecks
|
||||
-PackageKinds android-quest-apk)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_focus_apk_package_readiness
|
||||
COMMENT "Report Android Focus/Wave APK package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-AndroidNativeChecks
|
||||
-PackageKinds android-focus-apk)
|
||||
|
||||
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)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_linux_app_package_readiness
|
||||
COMMENT "Report Linux app package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds linux-app)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_webgl_package_readiness
|
||||
COMMENT "Report WebGL package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds 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()
|
||||
230
cmake/PanoPainterSources.cmake
Normal file
230
cmake/PanoPainterSources.cmake
Normal file
@@ -0,0 +1,230 @@
|
||||
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/legacy_canvas_document_io_services.cpp
|
||||
src/legacy_canvas_state_services.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_PLATFORM_LINUX_SOURCES
|
||||
src/platform_linux/linux_platform_services.cpp
|
||||
src/platform_linux/linux_platform_services.h
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_runtime.cpp
|
||||
src/app_cloud.cpp
|
||||
src/app_commands.cpp
|
||||
src/app_dialogs.cpp
|
||||
src/app_dialogs_info_openers.cpp
|
||||
src/app_events.cpp
|
||||
src/app_layout.cpp
|
||||
src/app_layout_about_layer_menu.cpp
|
||||
src/app_layout_file_menu.cpp
|
||||
src/app_layout_tools_menu.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
|
||||
src/platform_windows/windows_splash.cpp
|
||||
src/platform_windows/windows_splash.h
|
||||
src/platform_windows/windows_stylus_input.cpp
|
||||
src/platform_windows/windows_stylus_input.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.
|
||||
|
||||
1362
docs/modernization/build-inventory.md
Normal file
1362
docs/modernization/build-inventory.md
Normal file
File diff suppressed because it is too large
Load Diff
87
docs/modernization/capability-map.md
Normal file
87
docs/modernization/capability-map.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-16
|
||||
|
||||
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 |
|
||||
| UI ownership and thread affinity | `Node`, `LayoutManager`, `App` UI queue, retained callbacks | `pp_ui_core`, app runtime service, `pp_panopainter_ui` | Checked-handle dispatch, scoped callback disconnect, destroy-during-callback, close-during-dispatch, and UI-thread post/drain/shutdown coverage |
|
||||
| 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 |
|
||||
| Render/UI task dispatch and worker shutdown | `App`, `Canvas`, retained worker threads, platform entrypoints | app runtime service, `pp_foundation`, `pp_platform_*` | Render/UI queue order, same-thread dispatch, cancellation, shutdown drain, and no-detached-worker ownership coverage |
|
||||
| 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 |
|
||||
2417
docs/modernization/debt.md
Normal file
2417
docs/modernization/debt.md
Normal file
File diff suppressed because one or more lines are too long
234
docs/modernization/director-workflow.md
Normal file
234
docs/modernization/director-workflow.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Modernization Coordinator Workflow
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-16
|
||||
|
||||
Use this workflow when the user explicitly asks for subagents, delegation, a
|
||||
coordinator, 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`.
|
||||
|
||||
This file keeps the historical path name `director-workflow.md`, but the active
|
||||
model is now one coordinator managing workers directly. There is no captain
|
||||
layer.
|
||||
|
||||
## Goals
|
||||
|
||||
- Save main-thread tokens by keeping implementation and focused lookup out of
|
||||
the coordinator context.
|
||||
- Treat thread compaction as wasted budget. Prefer small active context,
|
||||
committed resume state, and fresh follow-on threads over carrying long stale
|
||||
history.
|
||||
- Keep each implementation slice measurable, validated, committed, and pushed.
|
||||
- Avoid merge conflicts by giving every worker a disjoint task and file scope.
|
||||
- Do not leave workers idle. Either give a worker another coherent follow-on
|
||||
task while its context is still useful, or close it promptly.
|
||||
- Reuse worker context only for closely related follow-on tasks in the same
|
||||
local area; otherwise close the worker and start a fresh one with a new
|
||||
minimal packet.
|
||||
- Keep prompts dense with the exact project/task context workers need so they do
|
||||
not spend tokens re-reading repo docs or re-parsing broad areas.
|
||||
- Keep communication terse: no fillers, no cheerleading, no narrative padding.
|
||||
Use direct technical wording only.
|
||||
|
||||
## Roles
|
||||
|
||||
### Coordinator
|
||||
|
||||
The coordinator is the main agent in the user thread. The coordinator owns:
|
||||
|
||||
- choosing the task group from `docs/modernization/tasks.md`
|
||||
- deciding whether delegation is worth the coordination cost
|
||||
- splitting work into direct worker-sized tasks
|
||||
- packaging the exact context each worker needs
|
||||
- spawning workers and explorers directly
|
||||
- integrating returned changes
|
||||
- running final validation
|
||||
- updating docs/debt/tasks
|
||||
- committing and pushing the verified slice
|
||||
- either assigning coherent follow-on work to an active worker or closing that
|
||||
worker once its useful context window is over
|
||||
|
||||
The coordinator should keep local work minimal. It may do a quick 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 directly to a worker whenever the scope can be
|
||||
made clear.
|
||||
|
||||
The coordinator must front-load context. Workers should not be told to "read
|
||||
the roadmap", "read AGENTS.md", or "inspect the repo" unless that is the task.
|
||||
The coordinator is responsible for extracting and passing:
|
||||
|
||||
- task ids and done checks
|
||||
- debt ids and removal conditions that matter
|
||||
- exact write scope and allowed read scope
|
||||
- required validation commands
|
||||
- the specific build/test preset or target names the worker may need
|
||||
- the exact code-exploration tools to use for the slice, such as `rg` or the
|
||||
compiler-aware `clangd_nav.py` helper
|
||||
- relevant file paths, code references, and current behavior notes
|
||||
- any repo rules or user constraints that materially affect the task
|
||||
|
||||
### Workers And Explorers
|
||||
|
||||
Workers perform bounded edits in assigned files. Explorers answer specific
|
||||
questions and should usually not edit files.
|
||||
|
||||
Workers do not own repo discovery. They start from the coordinator-provided
|
||||
context packet and stay inside the assigned scope unless they hit a blocker that
|
||||
requires a narrow follow-up question.
|
||||
|
||||
Workers may be kept alive for more than one assignment only when the next task
|
||||
is coherent with the current one: same subsystem, overlapping read scope,
|
||||
similar validation path, and no avoidable context rebuild. Do not keep workers
|
||||
alive just because a slot is available.
|
||||
|
||||
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
|
||||
- use the supplied task context first instead of broad repo/doc review
|
||||
- report changed files, validation run, and blockers
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Work Type | Model | Reasoning Effort | Use |
|
||||
| --- | --- | --- | --- |
|
||||
| Coordinator orchestration and integration | `gpt-5.4` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes, and worker packet preparation. |
|
||||
| Direct worker coding task | `gpt-5.4-mini` | `medium` | Bounded implementation in known files with coordinator-supplied context. |
|
||||
| Direct worker lookup or inventory | `gpt-5.4-mini` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
|
||||
| Mechanical docs cleanup | `gpt-5.4-mini` | `low` | Formatting, table updates, command normalization. |
|
||||
| Coordinator-only escalation | inherited higher model only when explicitly justified | inherited | Resolve architecture ambiguity, conflict integration, or task decomposition failures without adding a captain layer. |
|
||||
|
||||
Workers default to `gpt-5.4-mini`. If a task looks too broad or risky
|
||||
for that model, the coordinator should decompose it further or keep the narrow
|
||||
integration step locally instead of inserting an extra management tier.
|
||||
|
||||
## Context And 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 captured in the worker prompt.
|
||||
- Do not let the coordinator thread drift toward compaction. Once a verified
|
||||
slice is committed and pushed, prefer a fresh thread for the next slice if
|
||||
the remaining context is no longer tight.
|
||||
- Do not paste large logs into prompts. Point workers at log paths and ask for
|
||||
the smallest relevant excerpt.
|
||||
- Do not ask workers to broadly read `AGENTS.md`, the roadmap, the debt log, or
|
||||
other repo-wide docs. Summarize the exact rules and rows they need.
|
||||
- Default worker context to a minimal operating packet: task id, assigned file
|
||||
scope, build command, test command, and code-exploration command hints.
|
||||
- Keep worker prompts compact but complete. Shorter is good only if it still
|
||||
removes the need for worker-side repo rediscovery.
|
||||
- Ask for compact final reports: changed files, result, validation, blockers,
|
||||
next recommendation.
|
||||
- Keep active workers busy with another coherent task when that is cheaper than
|
||||
restarting context; otherwise close them immediately after integration.
|
||||
- Close workers that are done, blocked, or no longer have a strong context
|
||||
advantage so they do not accumulate and saturate worker slots.
|
||||
- Prefer the smallest number of concurrent workers that keeps disjoint work
|
||||
moving.
|
||||
- Use rolling integration: wait for whichever worker finishes first, process the
|
||||
result, then either reuse that worker for the next coherent slice or close it
|
||||
before launching a fresh worker for unrelated work.
|
||||
- Prefer committed repo state over chat history as the handoff mechanism between
|
||||
slices so worker and coordinator prompts stay short.
|
||||
|
||||
## Delegation Flow
|
||||
|
||||
1. Coordinator picks one or more `Ready` tasks from
|
||||
`docs/modernization/tasks.md` with disjoint write scopes.
|
||||
2. Coordinator splits each task into direct worker-sized units, grouping
|
||||
coherent follow-on work when one worker can finish it efficiently without
|
||||
broadening scope.
|
||||
3. Coordinator prepares a context packet for each worker with the exact task
|
||||
requirements, file scope, validation commands, and relevant project details.
|
||||
4. Coordinator assigns the task directly to a `gpt-5.4-mini` worker or
|
||||
explorer.
|
||||
5. Worker returns changed files, validation, blockers, and any narrow
|
||||
integration notes.
|
||||
6. Coordinator reviews for scope conflicts, integrates the result, and decides
|
||||
whether to give that same worker another coherent task or close it.
|
||||
7. Coordinator runs the listed validation command or the quiet checkpoint
|
||||
wrapper for each integrated slice.
|
||||
8. Coordinator updates `tasks.md`, `debt.md`, and `roadmap.md` if task state or
|
||||
documented behavior moved.
|
||||
9. Coordinator commits and pushes verified slices incrementally.
|
||||
|
||||
## Coordinator Prompt Template For A Worker
|
||||
|
||||
```text
|
||||
You are a `gpt-5.4-mini` worker on PanoPainter. Other agents may be
|
||||
editing nearby files; do not revert unrelated changes.
|
||||
|
||||
Task source: docs/modernization/tasks.md task(s) <TASK-ID-LIST>.
|
||||
Goal: <ONE PARAGRAPH>.
|
||||
Done checks: <DONE-CHECKS>.
|
||||
Debt ids: <DEBT-LIST>.
|
||||
Write scope: <FILES/DIRS ONLY>.
|
||||
Read scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
Code exploration: <RG OR CLANGD_NAV COMMANDS TO USE>.
|
||||
|
||||
Repo constraints you must follow:
|
||||
- <ONLY THE RELEVANT RULES>
|
||||
|
||||
Minimal context you should rely on instead of broad repo/doc review:
|
||||
- <CURRENT BEHAVIOR NOTE>
|
||||
- <RELEVANT FILE OR SYMBOL NOTE>
|
||||
- <WHY THIS SLICE IS SAFE / WHAT MUST NOT CHANGE>
|
||||
|
||||
Do not read repo-wide docs unless this task explicitly requires it. Use only
|
||||
the supplied context, the listed file scope, and the build/test/code-exploration
|
||||
commands above.
|
||||
|
||||
If the coordinator gives you a second task, accept it only when it is coherent
|
||||
with the current scope and does not require broad repo rediscovery. Otherwise
|
||||
say that a fresh worker should be used.
|
||||
|
||||
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 explicitly requires it. 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
|
||||
```
|
||||
|
||||
## Coordinator Prompt Template For An Explorer
|
||||
|
||||
```text
|
||||
Answer one codebase question for PanoPainter.
|
||||
|
||||
Question: <QUESTION>.
|
||||
Search scope: <FILES/DIRS>.
|
||||
Relevant context: <SHORT CONTEXT PACKET>.
|
||||
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.
|
||||
- Each worker result was integrated before that worker was reused.
|
||||
- Reused workers only handled coherent follow-on tasks with a real context
|
||||
advantage.
|
||||
- Done or blocked workers were closed instead of being left idle.
|
||||
- The coordinator did not carry unnecessary stale history when a fresh thread
|
||||
would have been cheaper than compaction.
|
||||
- The commit contains one coherent slice.
|
||||
- The branch was pushed.
|
||||
115
docs/modernization/renderer_api_contract.md
Normal file
115
docs/modernization/renderer_api_contract.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Renderer API Backend-Neutral Contract
|
||||
|
||||
## Purpose
|
||||
|
||||
`pp_renderer_api` defines the backend-neutral rendering contract used by `pp_paint_renderer`
|
||||
and the higher-level app core. This document captures the minimum behavior that any
|
||||
concrete backend (`pp_renderer_gl` today, Vulkan/Metal later) must preserve.
|
||||
|
||||
## Contract Scope
|
||||
|
||||
- Public interfaces:
|
||||
- `pp::renderer::IRenderDevice`
|
||||
- `pp::renderer::ICommandContext`
|
||||
- `pp::renderer::ITexture2D`
|
||||
- `pp::renderer::IRenderTarget`
|
||||
- `pp::renderer::IShaderProgram`
|
||||
- `pp::renderer::IMesh`
|
||||
- `pp::renderer::IReadbackBuffer`
|
||||
- `pp::renderer::IRenderTrace`
|
||||
- `pp::renderer::Recording*` helpers in `renderer_api/recording_renderer.*`
|
||||
- Validation helpers in `renderer_api.h` and shader catalog helpers in
|
||||
`renderer_api/shader_catalog.*`
|
||||
|
||||
## Behavioral Invariants
|
||||
|
||||
- No exceptions are part of API control flow; failures are reported through
|
||||
`pp::foundation::Status` / `pp::foundation::Result`.
|
||||
- Object lifetimes remain backend-owned; API consumers pass references/handles only.
|
||||
- Resource descriptors and command/state descriptors must be validated and constrained by the
|
||||
helper functions.
|
||||
- Backends may reject unsupported operations via explicit non-OK status but must not mutate
|
||||
visible program state before reporting failure.
|
||||
- Error codes and debug names are deterministic and backend-neutral (human-readable and
|
||||
test-stable where feasible).
|
||||
|
||||
## Surface Contracts
|
||||
|
||||
1. `IRenderDevice`
|
||||
|
||||
- `backend_name()` identifies backend family.
|
||||
- `features()` returns capability bits used for planner decisions.
|
||||
- Resource creation methods return `Result` and report allocation/validation failures.
|
||||
- `immediate_context()` is stable for the lifetime of the device object.
|
||||
- `trace()` may return `nullptr`; callers must tolerate no trace provider.
|
||||
|
||||
2. `ICommandContext`
|
||||
|
||||
- State mutation (`set_viewport`, `set_scissor`, blend/depth/shader/sampler/mesh/program binds) is
|
||||
explicit and backend-agnostic.
|
||||
- Command methods that can fail must return status.
|
||||
- `end_render_pass()` is always side-effect safe and non-throwing.
|
||||
- `read_texture` / `capture_frame` readback contracts are byte-sized and descriptor-driven.
|
||||
- Texture upload/copy/readback/transition methods must respect descriptor bounds and ordering rules.
|
||||
|
||||
3. Resource descriptors and helpers
|
||||
|
||||
- `TextureDesc`, `Extent2D`, `Viewport`, `ScissorRect`, `RenderPassDesc`,
|
||||
`TextureUsage`, `TextureState`, `BlendState`, `DepthState`, sampler/topology enums
|
||||
are shared semantic vocabulary across backends.
|
||||
- Validation helpers (`validate_*`) are the compatibility fence for contract behavior.
|
||||
- `PaintFeedbackPlan` and `plan_paint_feedback(...)` are the feature/algorithm decision seam
|
||||
for framebuffer feedback vs ping-pong workflows.
|
||||
|
||||
4. Trace and recording
|
||||
|
||||
- `IRenderTrace` is optional and may be elided, but implementations should support scoped markers
|
||||
and markers where used.
|
||||
- Recording backend (`RecordingRenderDevice`, `RecordingCommandContext`) must preserve command
|
||||
order and reject invalid sequences through status/command visibility.
|
||||
|
||||
## Feature semantics
|
||||
|
||||
Backends are expected to honor all feature bits consistently:
|
||||
|
||||
- `framebuffer_fetch`
|
||||
- `explicit_texture_transitions`
|
||||
- `texture_copy`
|
||||
- `render_target_blit`
|
||||
- `frame_capture`
|
||||
- `float16_render_targets`
|
||||
- `float32_render_targets`
|
||||
- `float32_linear_filtering`
|
||||
|
||||
Feature gates must be enforced by planners before issuing backend commands.
|
||||
|
||||
## Existing conformance coverage
|
||||
|
||||
Current renderer-api conformance tests (non-backend):
|
||||
|
||||
- `pp_renderer_api_tests`
|
||||
- `pp_renderer_api` test cases:
|
||||
- `validates_texture_usage_contract`
|
||||
- `validates_texture_transition_contract`
|
||||
- `validates_mipmap_generation_contract`
|
||||
- `validates_texture_copy_contract`
|
||||
- `validates_blit_contract`
|
||||
- `plans_paint_feedback_paths`
|
||||
- `renderer_interfaces_support_backend_neutral_dispatch`
|
||||
- `recording_renderer_*` command-sequence and validation tests
|
||||
|
||||
OpenGL-specific conformance remains in `pp_renderer_gl` suites:
|
||||
|
||||
- `pp_renderer_gl_capabilities_tests`
|
||||
- `pp_renderer_gl_command_plan_tests`
|
||||
- `pp_renderer_gl_gpu_readback_tests` (where GPU context is available)
|
||||
- `panopainter_renderer_conformance_matrix_self_test`
|
||||
- `ctest --preset renderer-conformance`
|
||||
- `panopainter_renderer_api_contract_self_test` (tooling guard for renderer API and paint renderer
|
||||
backend-neutral contract source purity).
|
||||
|
||||
## Open items for RND-007
|
||||
|
||||
- Ensure Vulkan/Metal planning/lifecycle tests run the same contract surfaces without backend leakage.
|
||||
- Keep `pp_renderer_api` implementation/usage free from backend-only headers and raw platform state.
|
||||
- Keep new backend labs opt-in until this contract and conformance matrix are complete.
|
||||
381
docs/modernization/roadmap.md
Normal file
381
docs/modernization/roadmap.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# PanoPainter Modernization Roadmap
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-16
|
||||
|
||||
This roadmap is now architecture-first.
|
||||
The active execution queue lives in `docs/modernization/tasks.md`.
|
||||
Completed and superseded task history moved to
|
||||
`docs/modernization/tasks-done.md`.
|
||||
The debt log remains `docs/modernization/debt.md`.
|
||||
|
||||
## Objective
|
||||
|
||||
Turn PanoPainter into a thin composition-root application over separable C++23
|
||||
components while preserving current behavior.
|
||||
|
||||
The target end state is not "more planners around the same legacy shell".
|
||||
The next phase is measured by ownership transfer in the live app, not by
|
||||
planner count, CLI breadth, or test count.
|
||||
The target end state is:
|
||||
|
||||
- real component ownership
|
||||
- real platform boundaries
|
||||
- real renderer boundaries
|
||||
- a thin `panopainter_app`
|
||||
- legacy containment targets either deleted or reduced to trivial,
|
||||
debt-tracked adapters
|
||||
|
||||
## What This Roadmap Covers
|
||||
|
||||
- app architecture
|
||||
- component boundaries
|
||||
- platform boundaries
|
||||
- renderer/app ownership boundaries
|
||||
- the order of work needed to finish the split
|
||||
|
||||
It does not try to be the full build, test, or automation reference.
|
||||
Those details live in the other modernization docs when needed.
|
||||
|
||||
## What Does Not Count As Top-Priority Progress
|
||||
|
||||
These can still be useful, but they are not first-order modernization work
|
||||
while the app shell still mostly looks like the old codebase:
|
||||
|
||||
- planner-only extraction that leaves the same live owner in place
|
||||
- new CLI surface without corresponding live app ownership reduction
|
||||
- test-only or automation-only expansion that does not unblock code movement
|
||||
- backend lab scaffolds
|
||||
- debt-log churn without a target or ownership change
|
||||
|
||||
## Reality Check
|
||||
|
||||
The codebase is meaningfully farther along than the old flat app, but it is not
|
||||
close to the final architecture yet.
|
||||
Historical percentage claims such as the earlier 67% score should not be used
|
||||
as a proxy for architectural completion. The live app still mostly runs through
|
||||
the same large shell and hotspot files.
|
||||
|
||||
What is already real:
|
||||
|
||||
- `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`
|
||||
|
||||
What is still carrying too much live ownership:
|
||||
|
||||
- `pp_panopainter_ui`: 34 files, about 9102 lines
|
||||
- `panopainter_app`: 29 files, about 8817 lines
|
||||
- `pp_legacy_paint_document`: 7 files, about 5709 lines
|
||||
- `pp_legacy_app`: 20 files, about 4368 lines
|
||||
- `pp_legacy_ui_core`: 20 files, about 3770 lines
|
||||
|
||||
Current hotspot files:
|
||||
|
||||
- `src/canvas.cpp`: 2645 lines
|
||||
- `src/app_layout.cpp`: 1249 lines
|
||||
- `src/canvas_modes.cpp`: 1798 lines
|
||||
- `src/node.cpp`: 1551 lines
|
||||
- `src/main.cpp`: 1117 lines
|
||||
- `src/node_panel_brush.cpp`: 1197 lines
|
||||
- `src/node_stroke_preview.cpp`: 933 lines
|
||||
- `src/node_canvas.cpp`: 953 lines
|
||||
- `src/app.cpp`: 950 lines
|
||||
- `src/app_dialogs.cpp`: 789 lines
|
||||
|
||||
Current architecture mismatches that must be treated as real blockers:
|
||||
|
||||
- `pp_platform_api` still compiles Apple implementation files instead of only
|
||||
platform-neutral policy and interface code.
|
||||
- `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I`
|
||||
directly, and Linux FPS title reporting now uses an injected callback, but
|
||||
retained Apple bridging in `platform_legacy` and other platform/app coupling
|
||||
remain, even though iOS keyboard visibility and prepared-file save handoff
|
||||
now also route through explicit Apple bridge callbacks and Apple render-
|
||||
context hooks plus iOS main-render-target binding now route through the same
|
||||
bridge style, as do Apple crash-test, app-close, and iOS SonarPen hooks,
|
||||
while Linux/Web GLFW render-context acquire/present and Linux app-close now
|
||||
route through retained local GLFW callback hooks, and retained Apple ObjC
|
||||
handles plus storage paths now sit behind one local `platform_legacy`
|
||||
helper instead of being re-read through `App::I` in each touched path, with
|
||||
the retained GLFW window hooks and fallback storage-path return now also
|
||||
using local retained-state helpers instead of direct method-body reads.
|
||||
- `src/platform_legacy/legacy_platform_services.*` is still part of the live
|
||||
app shell.
|
||||
- `pp_panopainter_ui` still depends on `pp_legacy_app`.
|
||||
- `Canvas`, `NodeCanvas`, and `NodeStrokePreview` still own too much live
|
||||
OpenGL execution around the renderer boundary, even though `NodeCanvas`
|
||||
display resolve, cache-to-screen composite, post-draw mask/grid/current-mode
|
||||
sequencing, per-layer/per-plane retained draw execution, and shared
|
||||
checkerboard background setup now route through retained draw-merge helpers,
|
||||
with the cache-to-screen checkerboard-plane callback setup also reduced and
|
||||
the merged-path checkerboard background-plane callback plus per-plane
|
||||
merged-texture draw callback plus non-`draw_merged` per-frame layer draw
|
||||
callback plus the smoothing-mask face shader/draw pass plus heightmap,
|
||||
current-mode, and grid-mode callback setup now routed through the same
|
||||
retained helper family, while post-draw callback assembly and the remaining
|
||||
per-layer render-path orchestration now also route through retained
|
||||
draw-merge helpers even though the broader node draw loop is still inline,
|
||||
with the non-`draw_merged` outer layer/plane traversal now also routing
|
||||
through `execute_legacy_canvas_draw_layer_traversal(...)`, while the heavier
|
||||
per-layer GL setup now also routes through
|
||||
`make_legacy_canvas_draw_merge_layer_path_gl_execution(...)` even though the
|
||||
remaining draw lambdas and broader node draw loop still live in
|
||||
`src/node_canvas.cpp`.
|
||||
- `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files
|
||||
rather than thin composition/binding surfaces, even though tools-menu binding
|
||||
plus nested panels/options submenu wiring now live in
|
||||
`src/app_layout_tools_menu.cpp` and `App::init_menu_tools()` is now a thin
|
||||
call-through, while file-menu binding plus the export submenu wiring now also
|
||||
live in `src/app_layout_file_menu.cpp` and `App::init_menu_file()` is now a
|
||||
thin call-through, while about-menu and layer-menu wiring now also live in
|
||||
`src/app_layout_about_layer_menu.cpp` and `App::init_menu_about()` plus
|
||||
`App::init_menu_layer()` are now thin call-throughs, while the informational
|
||||
overlay opener family now also lives in `src/app_dialogs_info_openers.cpp`
|
||||
and the corresponding `App::dialog_*` entrypoints are thinner.
|
||||
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
|
||||
global singleton reach, raw observer pointers, retained static worker
|
||||
ownership in several app families, and ad hoc mutex/condition-variable
|
||||
ownership, even though most previously detached or raw app-facing worker
|
||||
launches now use owned `std::jthread` or service-owned worker queues and
|
||||
`AppRuntime` now owns render/UI workers with explicit `std::jthread`
|
||||
shutdown semantics while the Windows splash-dialog and HMD renderer workers
|
||||
also use owned `std::jthread` lifecycle, `LogRemote` now uses the same
|
||||
ownership model, the Windows VR device now has explicit `std::unique_ptr`
|
||||
ownership instead of raw global lifetime, and the Windows main-loop/VR
|
||||
coordination flags now use `std::atomic` instead of unsynchronized globals,
|
||||
while the main Win32 entrypoint now groups window/GL/task/VR state behind a
|
||||
retained local state object instead of separate process-wide globals, the
|
||||
Win32 async GL/context lock state now lives under
|
||||
`src/platform_windows/windows_platform_services.cpp` instead of `main.cpp`
|
||||
retained state, the main-thread queued task state now sits behind a narrow
|
||||
retained helper instead of `RetainedState.main_tasklist` /
|
||||
`main_task_mutex`, the canvas async worker now sits behind a named retained
|
||||
local worker-state helper instead of a bare static accessor, the
|
||||
prepared-file worker and the canvas async import/export/save/open worker now
|
||||
live under `AppRuntime` instead of retained static app-events/canvas
|
||||
workers, and the splash-screen dialog loop, HWND ownership, and bitmap setup
|
||||
now live in `src/platform_windows/windows_splash.cpp` instead of
|
||||
`src/main.cpp`, while Win32 pointer API loading, stylus/ink timer decay,
|
||||
Wintab packet reset, and `WM_POINTERUPDATE` pen/touch handling now also live
|
||||
in `src/platform_windows/windows_stylus_input.cpp` instead of `src/main.cpp`,
|
||||
while the retained Win32 VR/HMD shell now also routes through
|
||||
`src/platform_windows/windows_vr_shell.h` instead of staying inline in
|
||||
`src/main.cpp`,
|
||||
while `App::rec_loop()` now delegates worker-iteration orchestration into
|
||||
the retained recording bridge, `App::update_rec_frames()` now delegates
|
||||
recording label refresh through that same retained recording path, and the
|
||||
canvas state-management cluster for picking, clear/clear-all, layer
|
||||
add/remove/order/lookups, animation frame control, resize, and snapshot
|
||||
save/restore now lives in `src/legacy_canvas_state_services.cpp` instead of
|
||||
`src/canvas.cpp`, while the larger import/export/save/open/thumbnail
|
||||
document-IO cluster now lives in `src/legacy_canvas_document_io_services.cpp`
|
||||
and `NodeStrokePreview` render-target setup plus immediate-pass sequencing
|
||||
now route through retained preview execution helpers, even though the bridge
|
||||
still owns worker-side readback flow and encoder-state label reads.
|
||||
- Modern C++23 usage exists in extracted components, especially `std::span`,
|
||||
explicit result/status objects, and a few concepts, but the live app still
|
||||
does not consistently express ownership, thread affinity, or renderer
|
||||
resources through safe component contracts.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- the base component extraction is real
|
||||
- the app shell thinning is not done
|
||||
- the platform split is not done
|
||||
- the UI split is not done
|
||||
- the renderer/app ownership split is not done
|
||||
- future backend lab work is still premature until those four statements change
|
||||
|
||||
## Final Target Architecture
|
||||
|
||||
The old roadmap showed a straight chain.
|
||||
That was too simple.
|
||||
The real target is a layered DAG with a thin composition root.
|
||||
|
||||
```text
|
||||
pp_foundation
|
||||
-> pp_assets
|
||||
-> pp_paint
|
||||
-> pp_document
|
||||
|
||||
pp_foundation
|
||||
-> pp_renderer_api
|
||||
-> pp_renderer_gl
|
||||
|
||||
pp_document + pp_paint + pp_renderer_api
|
||||
-> pp_paint_renderer
|
||||
|
||||
pp_foundation + pp_document
|
||||
-> pp_app_core
|
||||
|
||||
pp_foundation
|
||||
-> pp_ui_core
|
||||
|
||||
pp_platform_api
|
||||
-> pp_platform_windows
|
||||
-> pp_platform_apple
|
||||
-> pp_platform_linux
|
||||
-> pp_platform_android
|
||||
-> pp_platform_web
|
||||
-> pp_platform_vr
|
||||
|
||||
pp_app_core + pp_ui_core + pp_paint_renderer + pp_platform_api
|
||||
-> pp_panopainter_ui
|
||||
|
||||
pp_app_core + pp_panopainter_ui + pp_platform_*
|
||||
-> panopainter_app
|
||||
```
|
||||
|
||||
Key ownership rules:
|
||||
|
||||
- `pp_platform_api` is interface and policy only. No concrete platform service
|
||||
implementation files belong there.
|
||||
- `pp_platform_*` owns platform SDK, OS handles, platform event loops, and
|
||||
concrete service bridges.
|
||||
- `pp_ui_core` owns generic node/layout/overlay behavior and generic controls.
|
||||
- `pp_panopainter_ui` owns app-specific panels, dialogs, canvas views, and
|
||||
UI-to-app bindings.
|
||||
- `pp_app_core` owns planner logic, workflow policy, and service contracts. It
|
||||
does not own nodes, GL objects, or platform handles.
|
||||
- `pp_paint_renderer` owns renderer-facing paint/export/preview contracts.
|
||||
- `panopainter_app` owns composition only. It should stop being a second home
|
||||
for document workflow, dialog orchestration, platform state, or renderer
|
||||
execution.
|
||||
- Threading and task dispatch are app runtime services, not incidental static
|
||||
queues on `App` or detached workers launched from panels, dialogs, canvas, or
|
||||
cloud helpers.
|
||||
- UI ownership is handle/registry based at component boundaries. Raw `Node*`
|
||||
can remain as non-owning implementation detail only when lifetime is proven
|
||||
by checked handles or scoped connections.
|
||||
- Renderer-facing app code uses `pp_renderer_api` resources and command/context
|
||||
contracts. `Texture2D`, `RTT`, direct GL dispatch, and render-thread helpers
|
||||
must not leak into future-backend-facing UI or document code.
|
||||
|
||||
## Workstreams
|
||||
|
||||
### 1. Break The Canvas And Preview Hotspots First
|
||||
|
||||
This is the highest-value work because it moves the largest concentration of
|
||||
real app behavior out of the old shell.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- `canvas.cpp` loses major document-plus-render ownership
|
||||
- `node_canvas.cpp` and `node_stroke_preview.cpp` lose major render-pass
|
||||
orchestration
|
||||
- concrete GL execution moves behind renderer-facing services instead of living
|
||||
in app/node files
|
||||
|
||||
### 2. Thin The App Shell
|
||||
|
||||
`app.cpp`, `app_layout.cpp`, and `app_dialogs.cpp` must stop acting as mixed
|
||||
workflow, UI, and composition files.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- `app_layout.cpp` becomes menu/toolbar binding composition
|
||||
- `app_dialogs.cpp` becomes workflow dispatch plus retained dialog opening glue
|
||||
- `app.cpp` becomes startup/frame/queue composition over named helpers
|
||||
|
||||
### 3. Finish The UI Core Split
|
||||
|
||||
`pp_ui_core` exists, but generic widget ownership is still incomplete.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- generic `Node` and control code moves out of `pp_legacy_ui_core`
|
||||
- `pp_panopainter_ui` keeps only app-specific nodes
|
||||
- shared overlay/lifetime behavior stays centered in `pp_ui_core`
|
||||
- the scene graph has explicit ownership, non-owning references, scoped
|
||||
callback connections, and documented UI-thread affinity
|
||||
|
||||
### 4. Make Runtime And Thread Ownership Explicit
|
||||
|
||||
This is crucial for a modern app architecture and must move with the app-shell
|
||||
split, not after it.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- render/UI/worker queues are owned by explicit runtime services
|
||||
- detached worker threads are replaced by joinable/cancellable ownership or a
|
||||
task service with shutdown semantics
|
||||
- render-thread and UI-thread access are expressed through small contracts that
|
||||
can be implemented by future platform shells
|
||||
- `App::I` and `Canvas::I` stop being the way cross-thread code reaches state
|
||||
|
||||
### 5. Finish The Platform Split
|
||||
|
||||
This is still a real blocker, but it should follow the bulk code-moving work
|
||||
above instead of taking priority over the main app hotspots.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- remove concrete platform code from `pp_platform_api`
|
||||
- remove `App::I` reach from platform service implementations
|
||||
- remove app-owned cross-platform handle storage
|
||||
- reduce `platform_legacy` to thin composition or delete it
|
||||
|
||||
### 6. Retire Thick Workflow Bridges
|
||||
|
||||
Open/save/session/export/cloud/brush package flows are still too distributed
|
||||
across retained app, panel, and dialog files.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
- document workflow bridges become thin adapters over `pp_app_core`
|
||||
- cloud transfer and cloud browser ownership move out of retained UI nodes
|
||||
- brush package import/export ownership moves out of retained panel code and
|
||||
no longer depends on detached worker launch sites
|
||||
|
||||
### 7. Only Then Resume Future Backend Work
|
||||
|
||||
Vulkan, Metal, and package-only cleanup are explicitly downstream of the app
|
||||
architecture work above.
|
||||
|
||||
Do not treat future backend scaffolds as proof that modernization is near done
|
||||
while the current shell still depends on:
|
||||
|
||||
- `platform_legacy`
|
||||
- `pp_legacy_app`
|
||||
- `pp_legacy_ui_core`
|
||||
- `pp_legacy_paint_document`
|
||||
- large GL-heavy node and canvas files
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
The modernization is not done until these are all true:
|
||||
|
||||
- `pp_platform_api` contains only platform-neutral code
|
||||
- `src/platform_apple/*`, `src/platform_linux/*`, and other concrete platform
|
||||
implementations do not reach `App::I`
|
||||
- `platform_legacy` is gone or reduced to a trivial temporary shim
|
||||
- `App` no longer stores cross-platform handle state that belongs to platform
|
||||
shells
|
||||
- `pp_panopainter_ui` no longer depends on `pp_legacy_app`
|
||||
- `panopainter_app` is a composition root, not a second workflow layer
|
||||
- `canvas.cpp`, `node_canvas.cpp`, and `node_stroke_preview.cpp` no longer own
|
||||
large renderer-orchestration bodies
|
||||
- live app, UI, canvas, cloud, and platform code no longer launch detached
|
||||
worker threads without owned shutdown/cancellation
|
||||
- render/UI task queues are owned behind explicit app runtime services
|
||||
- raw app/UI pointers are non-owning implementation details only, backed by
|
||||
checked handles, scoped connections, or documented owner objects
|
||||
- future-backend-facing app and UI code depends on renderer API contracts, not
|
||||
retained OpenGL resource classes or direct GL execution
|
||||
- `pp_legacy_ui_core`, `pp_legacy_app`, and `pp_legacy_paint_document` are
|
||||
removed or reduced to narrow, explicit adapter seams with debt ids and clear
|
||||
removal conditions
|
||||
|
||||
## Active Queue
|
||||
|
||||
Use `docs/modernization/tasks.md` for the current architecture task bundles,
|
||||
ordered by real code-moving priority.
|
||||
Use `docs/modernization/tasks-done.md` only for history.
|
||||
5007
docs/modernization/tasks-done.md
Normal file
5007
docs/modernization/tasks-done.md
Normal file
File diff suppressed because it is too large
Load Diff
862
docs/modernization/tasks.md
Normal file
862
docs/modernization/tasks.md
Normal file
@@ -0,0 +1,862 @@
|
||||
# Modernization Task Tracker
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-16
|
||||
|
||||
This file now tracks only active architecture work.
|
||||
Completed, blocked, and superseded task history moved to
|
||||
`docs/modernization/tasks-done.md`.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Keep this file short. If a task is done, blocked for a long time, or no
|
||||
longer relevant, move it to `tasks-done.md` instead of letting the active
|
||||
queue sprawl.
|
||||
- Keep tasks architecture-first. Build, test, tool, planner, CLI, and
|
||||
automation cleanup are secondary unless they directly unblock or accompany a
|
||||
real ownership transfer in the live app.
|
||||
- Prefer coherent bundles over tiny adapter nibbles. Each task here should make
|
||||
visible progress in a hotspot file or a legacy target, not just add another
|
||||
seam around the same code.
|
||||
- Use legacy-target shrinkage and hotspot reduction as the main progress signal.
|
||||
If a slice does not materially reduce a large file or move code out of
|
||||
`PP_PANOPAINTER_*` or `PP_LEGACY_*`, it is probably not first-priority work.
|
||||
- Do not restart Vulkan, Metal, or package-only work until the app shell,
|
||||
platform split, UI split, and canvas/render split are materially thinner.
|
||||
|
||||
## Current Architecture Read
|
||||
|
||||
- The extracted pure targets are real and useful: `pp_foundation`,
|
||||
`pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`,
|
||||
`pp_paint_renderer`, `pp_ui_core`, `pp_platform_api`, and `pp_app_core`.
|
||||
- The remaining app still lives mostly in legacy containment or thick shell
|
||||
targets:
|
||||
- `pp_panopainter_ui`: 34 files, about 9102 lines
|
||||
- `panopainter_app`: 29 files, about 8817 lines
|
||||
- `pp_legacy_paint_document`: 7 files, about 5709 lines
|
||||
- `pp_legacy_app`: 20 files, about 4368 lines
|
||||
- `pp_legacy_ui_core`: 20 files, about 3770 lines
|
||||
- The biggest single-file choke points are still `src/canvas.cpp`,
|
||||
`src/app_layout.cpp`, `src/canvas_modes.cpp`, `src/node.cpp`,
|
||||
`src/main.cpp`, `src/node_panel_brush.cpp`, `src/node_stroke_preview.cpp`,
|
||||
`src/node_canvas.cpp`, `src/app.cpp`, and `src/app_dialogs.cpp`.
|
||||
- The platform boundary is not finished:
|
||||
- `pp_platform_api` still compiles Apple implementation files
|
||||
- `platform_apple` no longer reaches `App::I` directly, and Linux FPS title
|
||||
reporting now uses an injected callback, but retained Apple bridging and
|
||||
broader platform-to-app singleton reach are still open in
|
||||
`platform_legacy`
|
||||
- `platform_legacy` is still part of the live app shell
|
||||
- The app runtime boundary is not finished:
|
||||
- render/UI queues are static `App` state
|
||||
- app-facing detached launches are no longer the main issue; preview and
|
||||
recording now use owned worker threads, but those families still rely on
|
||||
retained static/global ownership and ad hoc runtime control
|
||||
- canvas async import/export/save/open now run through an owned in-file
|
||||
worker, but their retained progress execution is still not a clean runtime
|
||||
service boundary
|
||||
- thread-affinity rules are enforced by convention and asserts instead of
|
||||
explicit runtime contracts
|
||||
- The UI ownership boundary is not finished:
|
||||
- base `Node` still carries raw parent/manager pointers beside shared handles
|
||||
- callback captures still frequently close over retained nodes and `App::I`
|
||||
- checked overlay/lifetime helpers exist but are not yet the default scene
|
||||
graph model
|
||||
- Historical score/progress claims are not useful for prioritization here.
|
||||
The live app still mostly runs through the same shell and hotspot files, so
|
||||
the queue is now ordered by code movement instead.
|
||||
|
||||
## Active Bundles
|
||||
|
||||
### Priority Order
|
||||
|
||||
- `P0`: shrink the biggest live app hotspots and legacy ownership first
|
||||
- `P0`: make app runtime, UI ownership, and renderer access safe enough for
|
||||
future backend work
|
||||
- `P1`: finish supporting boundaries that unblock or stabilize the thinner app
|
||||
- `P2`: only then clean up the remaining workflow adapters
|
||||
|
||||
### Bundle 1 - Break The Canvas And Preview Hotspots
|
||||
|
||||
Priority: `P0`
|
||||
|
||||
Why this bundle is first:
|
||||
This is where the biggest block of real app behavior still lives.
|
||||
If these files stay large and stateful, the rest of the modernization still
|
||||
looks like a wrapper around the old renderer shell.
|
||||
|
||||
#### ARC-RND-001 - Split `canvas.cpp` Into Document State And Render Execution Shells
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
`src/canvas.cpp` is still the biggest single architectural blocker at about
|
||||
2645 lines.
|
||||
|
||||
Current slice:
|
||||
- Canvas state-management helpers for picking, clear/clear-all, layer
|
||||
add/remove/order/lookups, animation frame control, resize, and snapshot
|
||||
save/restore now live in `src/legacy_canvas_state_services.cpp` instead of
|
||||
staying inline in `src/canvas.cpp`, but the file still owns the larger
|
||||
document-plus-render shell.
|
||||
- Canvas import/export/save/open/thumbnail ownership now lives in
|
||||
`src/legacy_canvas_document_io_services.cpp` instead of staying inline in
|
||||
`src/canvas.cpp`, which materially reduces document IO ownership in the live
|
||||
render shell.
|
||||
|
||||
Write scope:
|
||||
- `src/canvas.cpp`
|
||||
- `src/canvas_layer.cpp`
|
||||
- `src/canvas_actions.cpp`
|
||||
- `src/legacy_canvas_*services*.h`
|
||||
- new `src/legacy_canvas_*.*` helpers if needed
|
||||
|
||||
Read scope:
|
||||
- `src/document/*`
|
||||
- `src/paint_renderer/*`
|
||||
- `src/renderer_api/*`
|
||||
|
||||
Done when:
|
||||
- `canvas.cpp` stops being the place where document state, render sequencing,
|
||||
history reads, and GL-side execution all meet.
|
||||
- Non-render state/query/update helpers move out first.
|
||||
- The remaining file reads as a render shell with explicit helper boundaries.
|
||||
- The touched slice makes a substantial reduction in inline ownership, not just
|
||||
a thin wrapper extraction.
|
||||
|
||||
Mini-model packet:
|
||||
- Do not try to delete `Canvas` in one slice.
|
||||
- Prioritize ownership separation over clever abstractions.
|
||||
- Aim for a visible reduction in `canvas.cpp`, on the order of hundreds of
|
||||
lines, not a token helper extraction.
|
||||
|
||||
#### ARC-RND-002 - Isolate Preview And Canvas View Render Execution
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
`src/node_stroke_preview.cpp` and `src/node_canvas.cpp` still own a large amount
|
||||
of live preview/canvas render sequencing around the renderer boundary.
|
||||
|
||||
Current slice:
|
||||
- `NodeStrokePreview` final composite plus preview-texture copy now route
|
||||
through `legacy_node_stroke_preview_execution_services.h`, but the preview
|
||||
node still owns most live-pass and retained GL resource execution.
|
||||
- `NodeStrokePreview` render-target setup plus immediate-pass sequence
|
||||
orchestration now also route through
|
||||
`legacy_node_stroke_preview_execution_services.h`, and duplicate render-
|
||||
target setup was removed from `render_to_image()` and the queued worker path,
|
||||
but the preview node still owns broader live-pass state and thread-facing
|
||||
orchestration.
|
||||
- `NodeCanvas` merged-path per-plane merged-texture draw execution now also
|
||||
routes through `execute_legacy_canvas_draw_merge_layer_texture(...)`.
|
||||
- `NodeCanvas` merged-path and non-blend checkerboard background setup now also
|
||||
route through `execute_legacy_canvas_draw_merge_background_setup(...)`.
|
||||
- `NodeCanvas` cache-to-screen checkerboard-plane callback setup now also routes
|
||||
through a retained helper in `legacy_canvas_draw_merge_services.h`.
|
||||
- `NodeCanvas` display resolve plus cache-to-screen checkerboard/cache-texture
|
||||
composite now route through `legacy_canvas_draw_merge_services.h`.
|
||||
- `NodeCanvas` smoothing-mask overlay, smoothing-mask face pass, grid keepalive
|
||||
draw, heightmap draw, and current-mode draw now also route through
|
||||
`execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw
|
||||
orchestration is still inline.
|
||||
- `NodeCanvas` heightmap draw callback setup now also routes through
|
||||
`make_legacy_canvas_draw_merge_heightmap_draw(...)`, but the node still owns
|
||||
current-mode traversal and broader post-draw orchestration.
|
||||
- `NodeCanvas` current-mode draw callback setup now also routes through
|
||||
`make_legacy_canvas_draw_merge_current_modes_draw(...)`, and grid-mode
|
||||
callback setup now also routes through
|
||||
`make_legacy_canvas_draw_merge_grid_modes_draw(...)`, but broader post-draw
|
||||
orchestration is still inline.
|
||||
- `NodeCanvas` checkerboard background plane callback setup now also routes
|
||||
through `make_legacy_canvas_draw_merge_background_checkerboard_plane(...)`,
|
||||
but the node still owns broader live layer traversal and renderer-state
|
||||
sequencing.
|
||||
- `NodeCanvas` merged-path per-plane merged-texture draw callback setup now
|
||||
also routes through `make_legacy_canvas_draw_merge_layer_texture_draw(...)`,
|
||||
but the node still owns broader live layer traversal and renderer-state
|
||||
sequencing.
|
||||
- `NodeCanvas` non-`draw_merged` per-frame layer draw callback setup now also
|
||||
routes through `make_legacy_canvas_draw_merge_layer_frame_draw(...)`, but
|
||||
the node still owns broader live layer traversal and renderer-state
|
||||
sequencing.
|
||||
- `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution
|
||||
now also route through
|
||||
`execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns
|
||||
the broader canvas draw flow and renderer-state sequencing around that seam.
|
||||
- `NodeCanvas` non-`draw_merged` per-layer/per-plane retained draw execution
|
||||
now also routes through `execute_legacy_canvas_draw_merge_layer_plane(...)`,
|
||||
but the node still owns substantial live layer traversal and renderer state
|
||||
orchestration.
|
||||
- `NodeCanvas` post-draw callback assembly now also routes through
|
||||
`execute_legacy_canvas_draw_merge_post_draw_callbacks(...)`, and the
|
||||
remaining per-layer render-path orchestration now also routes through
|
||||
`execute_legacy_canvas_draw_merge_layer_path(...)`, but the node still owns
|
||||
broader draw-loop and renderer-state shell sequencing.
|
||||
- `NodeCanvas` outer non-`draw_merged` layer/plane traversal, onion-range
|
||||
failure handling, and visit payload setup now also route through
|
||||
`execute_legacy_canvas_draw_layer_traversal(...)`.
|
||||
- `NodeCanvas` non-`draw_merged` per-layer temporary erase/paint, layer-texture,
|
||||
and blend setup now also route through
|
||||
`make_legacy_canvas_draw_merge_layer_path_gl_execution(...)`, but the node
|
||||
still owns the remaining draw lambdas and broader renderer-state shell.
|
||||
|
||||
Write scope:
|
||||
- `src/node_stroke_preview.cpp`
|
||||
- `src/node_canvas.cpp`
|
||||
- `src/legacy_node_stroke_preview_execution_services.h`
|
||||
- `src/legacy_canvas_stroke_preview_services.h`
|
||||
- `src/paint_renderer/compositor.*`
|
||||
|
||||
Read scope:
|
||||
- `src/renderer_api/*`
|
||||
- `src/renderer_gl/*`
|
||||
|
||||
Done when:
|
||||
- Preview/canvas files stop carrying large inline render-pass orchestration.
|
||||
- Concrete GL texture/bind/copy work is pushed behind explicit renderer-facing
|
||||
service seams.
|
||||
- The next renderer backend would have one place to implement preview/canvas
|
||||
execution contracts instead of reading node code.
|
||||
- The touched node files are materially smaller and less stateful afterward.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep the existing `pp_paint_renderer` planner surface and extend it only when
|
||||
the node files clearly need a missing renderer-owned callback contract.
|
||||
- Prefer deleting inline orchestration over adding another planner layer around
|
||||
the same node code.
|
||||
|
||||
#### ARC-RND-003 - Replace App-Facing `Texture2D`/`RTT` Use With Renderer API Contracts
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Future Vulkan and Metal work needs the live app to stop treating retained
|
||||
OpenGL resource classes as the renderer boundary.
|
||||
|
||||
Write scope:
|
||||
- `src/node_canvas.cpp`
|
||||
- `src/node_stroke_preview.cpp`
|
||||
- `src/canvas.cpp`
|
||||
- `src/texture.*`
|
||||
- `src/rtt.*`
|
||||
- `src/renderer_api/*`
|
||||
- `src/paint_renderer/*`
|
||||
|
||||
Read scope:
|
||||
- `src/renderer_gl/*`
|
||||
- `src/legacy_gl_*dispatch.h`
|
||||
- `src/app_shaders.cpp`
|
||||
|
||||
Done when:
|
||||
- App/UI-facing render work talks to `pp_renderer_api` resource and command
|
||||
abstractions or narrow retained renderer services, not directly to
|
||||
`Texture2D`, `RTT`, or GL dispatch helpers.
|
||||
- The remaining `Texture2D`/`RTT` references are contained in retained GL
|
||||
backend/adapters with explicit removal conditions.
|
||||
- Canvas, preview, and export paths expose the same execution contract a Vulkan
|
||||
or Metal implementation would need.
|
||||
- The touched slice shrinks app/UI render ownership instead of only renaming
|
||||
wrappers.
|
||||
|
||||
Mini-model packet:
|
||||
- Do not start a Vulkan or Metal backend in this task.
|
||||
- Use existing `pp_renderer_api` and `pp_paint_renderer` contracts first.
|
||||
- Treat direct GL classes in UI/app code as debt to move behind backend-owned
|
||||
services.
|
||||
|
||||
### Bundle 2 - Thin The App Shell
|
||||
|
||||
Priority: `P0`
|
||||
|
||||
Why this bundle is next:
|
||||
`app_layout.cpp`, `app_dialogs.cpp`, and `app.cpp` still make the modernized
|
||||
targets look like helpers under one old monolith.
|
||||
|
||||
#### ARC-APP-001 - Split `app_layout.cpp` Into UI Binding Modules
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
`src/app_layout.cpp` is still a 1249-line mixed file that builds menus,
|
||||
attaches callbacks, computes planner inputs, and mutates UI state directly.
|
||||
|
||||
Current slice:
|
||||
- Tools-menu UI binding, including the nested panels/options submenu wiring,
|
||||
now lives in `src/app_layout_tools_menu.cpp`, and `App::init_menu_tools()`
|
||||
is now a thin call-through, but file/about/layer/sidebar and broader layout
|
||||
composition are still inline in `src/app_layout.cpp`.
|
||||
- File-menu UI binding, including the export submenu wiring, now also lives in
|
||||
`src/app_layout_file_menu.cpp`, and `App::init_menu_file()` is now a thin
|
||||
call-through, but about/layer/sidebar and broader layout composition are
|
||||
still inline in `src/app_layout.cpp`.
|
||||
- About-menu and layer-menu wiring now also live in
|
||||
`src/app_layout_about_layer_menu.cpp`, and `App::init_menu_about()` plus
|
||||
`App::init_menu_layer()` are now thin call-throughs, but edit/sidebar and
|
||||
broader layout composition are still inline in `src/app_layout.cpp`.
|
||||
|
||||
Write scope:
|
||||
- `src/app_layout.cpp`
|
||||
- `src/legacy_app_shell_services.*`
|
||||
- new `src/app_layout_*.*` or `src/legacy_*_ui_services.*` files if needed
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/*menu*.h`
|
||||
- `src/app_core/brush_ui.h`
|
||||
- `src/app_core/document_layer.h`
|
||||
- `src/app_core/main_toolbar.h`
|
||||
|
||||
Done when:
|
||||
- `app_layout.cpp` becomes a composition/binding file instead of a giant mixed
|
||||
controller.
|
||||
- File-menu, toolbar, tools-menu, about-menu, and layer-menu wiring each live
|
||||
in named helper modules or services.
|
||||
- The split follows planner/service boundaries already present in `pp_app_core`.
|
||||
- The touched slice materially shrinks the file instead of just moving a few
|
||||
lambdas around.
|
||||
|
||||
Mini-model packet:
|
||||
- Start by carving out one coherent family at a time, not by reshuffling lines.
|
||||
- Preserve the current planner calls; the goal is ownership, not new behavior.
|
||||
- Aim for a real file-size drop, not cosmetic decomposition.
|
||||
|
||||
#### ARC-APP-002 - Split `app_dialogs.cpp` Into Workflow Adapters And Widget Openers
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`src/app_dialogs.cpp` still mixes document workflow decisions, export routing,
|
||||
dialog construction, and overlay ownership.
|
||||
|
||||
Current slice:
|
||||
- Informational overlay opener paths for user manual, changelog, about,
|
||||
what's-new, and shortcuts now live in `src/app_dialogs_info_openers.cpp`,
|
||||
and the corresponding `App::dialog_*` entrypoints are now thin call-throughs,
|
||||
but document/export workflow and retained dialog execution are still inline in
|
||||
`src/app_dialogs.cpp`.
|
||||
|
||||
Write scope:
|
||||
- `src/app_dialogs.cpp`
|
||||
- `src/legacy_app_dialog_services.*`
|
||||
- `src/legacy_document_session_services.*`
|
||||
- `src/legacy_document_open_services.*`
|
||||
- `src/legacy_document_export_services.*`
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/app_dialog.h`
|
||||
- `src/app_core/document_session.h`
|
||||
- `src/app_core/document_export.h`
|
||||
|
||||
Done when:
|
||||
- `app_dialogs.cpp` is reduced to thin entrypoints plus named helper modules.
|
||||
- Dialog creation/opening is clearly separated from document/export workflow
|
||||
routing.
|
||||
- The remaining direct node-specific code is isolated to retained dialog
|
||||
adapters.
|
||||
- The slice removes a meaningful amount of mixed live ownership from
|
||||
`app_dialogs.cpp`.
|
||||
|
||||
Mini-model packet:
|
||||
- Preserve existing planner usage.
|
||||
- Prefer new narrow helper files over leaving another giant dialog utility file.
|
||||
- Do not spend time extending dialog planners or CLI surfaces unless the live
|
||||
adapter gets thinner in the same slice.
|
||||
|
||||
#### ARC-APP-003 - Reduce `app.cpp` To Frame, Queue, And Composition Shell
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`src/app.cpp` still carries startup, frame flow, queue draining, recording,
|
||||
observer math, and composition logic in one 950-line file.
|
||||
|
||||
Write scope:
|
||||
- `src/app.cpp`
|
||||
- `src/legacy_app_startup_services.*`
|
||||
- `src/legacy_recording_services.*`
|
||||
- small new `src/app_runtime_*.*` helpers if needed
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/app_frame.h`
|
||||
- `src/app_core/app_shutdown.h`
|
||||
- `src/app_core/app_startup.h`
|
||||
- `src/app_core/document_recording.h`
|
||||
|
||||
Done when:
|
||||
- `app.cpp` reads like a shell over `pp_app_core` planners and named retained
|
||||
services.
|
||||
- Startup, frame/update, queue/thread, and recording glue are split into named
|
||||
helpers instead of living inline.
|
||||
- `App` keeps ownership of composition state only where it truly has to.
|
||||
- The file becomes materially thinner in the same slice.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep thread behavior unchanged.
|
||||
- Split by responsibility boundaries already present in `pp_app_core`.
|
||||
- Prefer moving live ownership out over creating new planner wrappers.
|
||||
|
||||
#### ARC-APP-004 - Move Render/UI Queues Into An Owned App Runtime Service
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
`App` still owns static render/UI queues, mutexes, condition variables, and
|
||||
thread ids. That makes thread safety hard to reason about and keeps platform
|
||||
entrypoints coupled to the singleton.
|
||||
|
||||
Current slice:
|
||||
- render/UI queues, mutexes, condition variables, and thread identity already
|
||||
live in `AppRuntime`
|
||||
- `AppRuntime` render/UI worker ownership now also uses `std::jthread` plus
|
||||
explicit stop requests instead of raw `std::thread`
|
||||
- Windows main-loop run-state and VR worker coordination flags in `main.cpp`
|
||||
now use `std::atomic` ownership instead of unsynchronized globals
|
||||
- `main.cpp` Win32 window handles, GL task/mutex state, splash-dialog state,
|
||||
stylus timers, and VR worker state now sit behind one retained local state
|
||||
object instead of separate file-scope globals
|
||||
- the splash-screen dialog loop, HWND ownership, and bitmap setup now also live
|
||||
in `src/platform_windows/windows_splash.cpp` instead of `src/main.cpp`, and
|
||||
`main.cpp` now just owns the helper lifecycle
|
||||
- Win32 async GL/context lock state now lives in
|
||||
`src/platform_windows/windows_platform_services.cpp` instead of `main.cpp`
|
||||
retained state, and `main.cpp` only seeds that platform-owned context handle
|
||||
pair during initialization and context recreation
|
||||
- `main.cpp` main-thread queued task state now sits behind a narrow retained
|
||||
helper instead of `RetainedState.main_tasklist` / `main_task_mutex` directly
|
||||
- Win32 pointer API loading, stylus/ink timer ownership and decay, `WT_PACKET`
|
||||
reset, and `WM_POINTERUPDATE` pen/touch handling now live in
|
||||
`src/platform_windows/windows_stylus_input.cpp` instead of `src/main.cpp`,
|
||||
but broader retained Win32 shell state is still open
|
||||
- the retained Win32 VR/HMD shell, including worker start/stop and VR FPS
|
||||
state, now routes through `src/platform_windows/windows_vr_shell.h` instead
|
||||
of staying inline in `src/main.cpp`, but broader retained Win32 shell state
|
||||
is still open
|
||||
- prepared-file background work now runs through an `AppRuntime`-owned worker
|
||||
queue instead of a retained static worker in `src/app_events.cpp`
|
||||
- canvas async import/export/save/open background work now also runs through an
|
||||
`AppRuntime`-owned worker queue instead of a retained static worker in
|
||||
`src/canvas.cpp`
|
||||
- retained `App` composition, task call sites, and platform/runtime entrypoint
|
||||
coupling are still not fully reduced behind the runtime contract
|
||||
|
||||
Write scope:
|
||||
- `src/app.h`
|
||||
- `src/app.cpp`
|
||||
- `src/app_events.cpp`
|
||||
- `src/main.cpp`
|
||||
- new `src/app_runtime_*.*` or retained runtime service files if needed
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/app_thread.h`
|
||||
- `src/platform_api/platform_services.h`
|
||||
- render/UI task call sites under `src/*.cpp`
|
||||
|
||||
Done when:
|
||||
- Render and UI task queues are owned by an explicit runtime object or service
|
||||
with startup, drain, stop, and thread-affinity APIs.
|
||||
- `App` composes that runtime instead of exposing static global queue state.
|
||||
- Platform event code and retained services post work through the runtime
|
||||
contract rather than by reaching `App::I` static queues.
|
||||
- Shutdown behavior remains deterministic and the touched slice reduces
|
||||
singleton/thread coupling.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep public behavior and thread ordering unchanged.
|
||||
- Prefer a small runtime owner over broad task-system redesign.
|
||||
- Make ownership and shutdown semantics explicit before adding new features.
|
||||
|
||||
#### ARC-APP-005 - Replace Detached App Workers With Joinable Or Service-Owned Work
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
The biggest app-facing async families have been moved off detached launches,
|
||||
but retained worker ownership and ad hoc runtime control are still not a safe
|
||||
modernization foundation.
|
||||
|
||||
Current slice:
|
||||
- app-owned render/UI runtime queues and cloud worker ownership are already
|
||||
moving behind owned runtime/service objects
|
||||
- Windows splash-dialog and HMD renderer worker ownership in `main.cpp` now
|
||||
also use `std::jthread` with explicit stop requests instead of raw
|
||||
`std::thread`
|
||||
- Windows VR device ownership in `main.cpp` now also uses `std::unique_ptr`
|
||||
instead of a raw `Vive*`
|
||||
- `LogRemote` worker ownership in `src/log.*` now also uses `std::jthread`
|
||||
with explicit stop requests instead of raw `std::thread`
|
||||
- brush package import/export now use service-owned `std::jthread` workers and
|
||||
UI-thread completion handoff
|
||||
- prepared-file save work and grid lightmap launch now also use service-owned
|
||||
workers with explicit UI-thread handoff
|
||||
- canvas async import/export/save/open and timelapse export now also use owned
|
||||
worker queues instead of detached threads
|
||||
- `src/app_events.cpp` prepared-file worker ownership and `src/canvas.cpp`
|
||||
async import/export/save/open worker ownership now also sit behind named
|
||||
retained local worker-state helpers instead of bare static worker accessors
|
||||
- the prepared-file worker and the broader canvas async import/export/save/open
|
||||
worker have now both moved into `AppRuntime`, removing the retained static
|
||||
workers from `src/app_events.cpp` and `src/canvas.cpp`
|
||||
- preview background rendering, recording, and the retained
|
||||
`NodePanelGrid::bake_uvs()` worker now also use `std::jthread`, but their
|
||||
retained loop/control flow is still open
|
||||
- `App::rec_loop()` now delegates its worker-iteration orchestration into the
|
||||
retained recording bridge in `src/legacy_recording_services.cpp`, while
|
||||
`App::update()` no longer carries the dead update mutex residue; retained
|
||||
recording loop control, readback ownership, and MP4 execution are still open
|
||||
- `App::update_rec_frames()` now delegates recording label refresh through
|
||||
`src/legacy_recording_services.cpp`, but retained recording label lookup,
|
||||
encoder-state reads, and MP4 execution still stay on the legacy bridge
|
||||
|
||||
Write scope:
|
||||
- `src/canvas.cpp`
|
||||
- `src/app_cloud.cpp`
|
||||
- `src/app_events.cpp`
|
||||
- `src/legacy_cloud_services.*`
|
||||
- `src/legacy_brush_package_import_services.*`
|
||||
- `src/legacy_brush_package_export_services.*`
|
||||
- `src/legacy_grid_ui_services.*`
|
||||
- `src/node_dialog_cloud.*`
|
||||
- `src/node_stroke_preview.*`
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/app_thread.h`
|
||||
- `src/foundation/task_queue.*`
|
||||
- `src/legacy_recording_services.*`
|
||||
|
||||
Done when:
|
||||
- Touched worker families are owned by joinable `std::jthread`, a scoped worker
|
||||
object, or an injected task service with cancellation/shutdown semantics.
|
||||
- Worker callbacks do not capture raw retained nodes or `this` across unknown
|
||||
lifetime without a checked handle, weak ownership, or explicit owner.
|
||||
- App shutdown can stop the touched worker family without racing UI/layout or
|
||||
renderer destruction.
|
||||
- Detached `std::thread` count drops materially in app-facing code.
|
||||
|
||||
Mini-model packet:
|
||||
- Start with one coherent worker family, such as cloud or brush package import.
|
||||
- Do not rewrite all threading at once.
|
||||
- Preserve the existing UI/progress behavior while changing ownership.
|
||||
|
||||
### Bundle 3 - Finish The UI Core Split
|
||||
|
||||
Priority: `P0`
|
||||
|
||||
Why this bundle is still top priority:
|
||||
Until generic `Node` and control code leaves `pp_legacy_ui_core`, the UI
|
||||
architecture remains mostly the old one with a modern overlay/lifetime helper
|
||||
attached to it.
|
||||
|
||||
#### ARC-UI-001 - Move Generic Node And Control Code Out Of `pp_legacy_ui_core`
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`pp_ui_core` has layout, color, node lifetime, and overlay lifetime, but the
|
||||
generic widget layer still sits in `pp_legacy_ui_core`.
|
||||
|
||||
Write scope:
|
||||
- `src/node.cpp`
|
||||
- `src/layout.cpp`
|
||||
- generic `src/node_*` base control files from `PP_LEGACY_UI_CORE_SOURCES`
|
||||
- `src/ui_core/*`
|
||||
- `CMakeLists.txt`
|
||||
- `cmake/PanoPainterSources.cmake`
|
||||
|
||||
Read scope:
|
||||
- `src/node_panel_*`
|
||||
- `src/node_dialog_*`
|
||||
|
||||
Done when:
|
||||
- Generic controls and base node/layout code are clearly separated from
|
||||
PanoPainter-specific panels and dialogs.
|
||||
- `pp_ui_core` grows as the home of generic widgets and node behavior.
|
||||
- `pp_panopainter_ui` keeps only app-specific panels, dialogs, canvas, preview,
|
||||
and workflow nodes.
|
||||
- The touched slice removes real file ownership from `pp_legacy_ui_core`, not
|
||||
just adds wrappers around it.
|
||||
|
||||
Mini-model packet:
|
||||
- Start with the controls that have no app-specific policy:
|
||||
button, checkbox, icon, image, scroll, slider, text, text input.
|
||||
- Do not mix panel/dialog rewrites into the same slice.
|
||||
- Prefer target ownership moves over purely internal helper reshuffles.
|
||||
|
||||
#### ARC-UI-002 - Make Checked Handles The Default UI Ownership Model
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`pp_ui_core` has lifetime and overlay handle helpers, but retained UI code still
|
||||
mixes raw `Node*`, shared ownership, direct `add_child(...)`, and callback
|
||||
captures across mutation points.
|
||||
|
||||
Write scope:
|
||||
- `src/node.*`
|
||||
- `src/layout.*`
|
||||
- `src/legacy_ui_overlay_services.*`
|
||||
- retained `src/node_dialog_*` and `src/node_panel_*` files touched by a slice
|
||||
- `src/ui_core/node_lifetime.*`
|
||||
- `src/ui_core/overlay_lifetime.*`
|
||||
|
||||
Read scope:
|
||||
- existing popup/dialog call sites found with `add_child`, `remove_child`, and
|
||||
`on_*` callback captures
|
||||
|
||||
Done when:
|
||||
- New or touched UI surfaces open, close, and dispatch callbacks through checked
|
||||
handles or scoped connections by default.
|
||||
- Raw `Node*` fields and callback parameters are documented or reshaped as
|
||||
non-owning views, not lifetime owners.
|
||||
- Destroy-during-callback and close-during-dispatch behavior is owned by
|
||||
`pp_ui_core` rather than each panel/dialog.
|
||||
- App-specific panels become view/controller shells over safe UI-core
|
||||
lifetime primitives.
|
||||
|
||||
Mini-model packet:
|
||||
- Convert one popup/dialog family at a time.
|
||||
- Do not redesign the UI appearance in this task.
|
||||
- Prefer deleting raw lifetime assumptions over adding more guard comments.
|
||||
|
||||
#### ARC-UI-003 - Split UI Rendering From Scene Graph And App State
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
The generic node layer still mixes layout, input, rendering, direct app access,
|
||||
and retained OpenGL resource usage. That blocks both a cleaner UI component and
|
||||
future renderer backends.
|
||||
|
||||
Write scope:
|
||||
- `src/node.cpp`
|
||||
- `src/node_canvas.cpp`
|
||||
- `src/node_stroke_preview.cpp`
|
||||
- generic control files moved toward `src/ui_core/*`
|
||||
- `src/renderer_api/*`
|
||||
- `src/paint_renderer/*`
|
||||
|
||||
Read scope:
|
||||
- `src/font.*`
|
||||
- `src/shape.*`
|
||||
- `src/texture.*`
|
||||
- `src/rtt.*`
|
||||
|
||||
Done when:
|
||||
- Scene graph/layout/input code has a renderer-neutral draw contract.
|
||||
- Generic controls do not need to know app singleton state or concrete GL
|
||||
resource classes.
|
||||
- App-specific canvas and preview rendering depends on renderer-facing services
|
||||
rather than base `Node` internals.
|
||||
- The touched slice makes `pp_ui_core` more reusable without hiding app policy
|
||||
inside it.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep visual behavior unchanged.
|
||||
- Do not move PanoPainter-specific panel policy into `pp_ui_core`.
|
||||
- Use renderer-neutral contracts first; backend implementation follows later.
|
||||
|
||||
### Bundle 4 - Make The Platform Boundary Real
|
||||
|
||||
Priority: `P1`
|
||||
|
||||
Why this bundle is not `P0`:
|
||||
It matters, but moving platform code first will not change the day-to-day shape
|
||||
of the app as much as reducing `canvas.cpp`, the app shell, and the generic UI
|
||||
layer.
|
||||
|
||||
#### ARC-PLT-001 - Split `pp_platform_api` From Concrete Platform Code
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`pp_platform_api` is supposed to be the SDK-free policy and interface layer,
|
||||
but it still compiles `src/platform_apple/apple_platform_services.*`.
|
||||
|
||||
Write scope:
|
||||
- `CMakeLists.txt`
|
||||
- `src/platform_api/*`
|
||||
- `src/platform_apple/*`
|
||||
|
||||
Read scope:
|
||||
- `cmake/PanoPainterSources.cmake`
|
||||
- `src/platform_windows/*`
|
||||
- `src/platform_linux/*`
|
||||
|
||||
Done when:
|
||||
- `pp_platform_api` contains only platform-neutral interfaces, policies, and
|
||||
shared helpers.
|
||||
- Apple implementation files are built by a concrete platform target instead of
|
||||
the API target.
|
||||
- The dependency direction is obvious from CMake without reading debt notes.
|
||||
|
||||
Mini-model packet:
|
||||
- Start in `CMakeLists.txt` around `pp_platform_api`.
|
||||
- Keep the change structural; do not broaden into new feature work.
|
||||
- Preserve current Apple service entrypoints while moving ownership.
|
||||
|
||||
#### ARC-PLT-002 - Remove `App::I` Reach From Apple And Linux Services
|
||||
|
||||
Status: In Progress
|
||||
|
||||
Why now:
|
||||
The current Apple and Linux service files still call into the app singleton,
|
||||
which means the platform layer is not a platform layer yet.
|
||||
|
||||
Current slice:
|
||||
- Linux FPS title updates now route through an injected callback installed from
|
||||
`App::set_platform_services()`
|
||||
- `platform_apple` clipboard, display/share, cursor, and save-ui-state calls
|
||||
now route through injected Apple bridge callbacks instead of `App::I`
|
||||
- `LegacyPlatformServices::prepare_storage_paths()` now routes Apple path
|
||||
preparation through a narrow local helper instead of reading `App::I`
|
||||
directly in that method body
|
||||
- iOS virtual-keyboard visibility and prepared-file save handoff now also route
|
||||
through explicit Apple bridge callbacks instead of direct `App::I` calls in
|
||||
`LegacyPlatformServices`
|
||||
- Apple render-context acquire/release/present hooks and iOS
|
||||
`bind_main_render_target()` now also route through explicit Apple bridge
|
||||
callbacks instead of direct `App::I` calls in `LegacyPlatformServices`
|
||||
- Apple crash-test, app-close, and iOS SonarPen hooks now also route through
|
||||
explicit Apple bridge callbacks instead of direct `App::I` calls in
|
||||
`LegacyPlatformServices`
|
||||
- retained Apple ObjC handles plus storage paths now live in one local
|
||||
`platform_legacy` helper, and the iOS SonarPen bridge now starts through
|
||||
that retained Apple state instead of reading `App::I` inside the bridge body
|
||||
- Linux/Web GLFW render-context acquire/present hooks and Linux app-close now
|
||||
also route through retained local GLFW callback hooks instead of direct
|
||||
method-body `App::I` access in `LegacyPlatformServices`
|
||||
- retained GLFW window hooks and the non-Linux fallback storage-path return now
|
||||
also route through retained local state helpers instead of reading `App::I`
|
||||
directly in those method bodies
|
||||
- retained Apple callback injection and broader `platform_legacy` singleton
|
||||
reach are still open
|
||||
|
||||
Write scope:
|
||||
- `src/platform_apple/*`
|
||||
- `src/platform_linux/*`
|
||||
- `src/app_events.cpp`
|
||||
- `src/app.h`
|
||||
|
||||
Read scope:
|
||||
- `src/platform_api/platform_services.h`
|
||||
- `src/platform_legacy/legacy_platform_services.*`
|
||||
|
||||
Done when:
|
||||
- `src/platform_apple/*` and `src/platform_linux/*` no longer call `App::I`.
|
||||
- The app injects the minimum callbacks or bridge state those services need.
|
||||
- Platform files stop depending on app-global state for clipboard, sharing, FPS
|
||||
title updates, or native UI saves.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep the interface small. Prefer injected callbacks/bridges over passing the
|
||||
whole `App`.
|
||||
- Do not rewrite Windows in the same slice.
|
||||
|
||||
#### ARC-PLT-003 - Remove App-Owned Cross-Platform Handle Storage
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`src/platform_legacy/legacy_platform_services.cpp` and `src/app.h` still keep
|
||||
platform-handle state on `App`, which blocks a real `pp_platform_*` shell split.
|
||||
|
||||
Write scope:
|
||||
- `src/platform_legacy/legacy_platform_services.*`
|
||||
- `src/app.h`
|
||||
- `src/app_events.cpp`
|
||||
- `src/platform_windows/*`
|
||||
|
||||
Read scope:
|
||||
- `src/main.cpp`
|
||||
- Apple/Android/Web/Linux entrypoint files only as needed
|
||||
|
||||
Done when:
|
||||
- `App` no longer owns platform-specific handle fields that belong to shells.
|
||||
- The legacy platform adapter becomes thin composition or disappears for the
|
||||
touched path.
|
||||
- Platform setup state lives with the relevant `pp_platform_*` implementation.
|
||||
|
||||
Mini-model packet:
|
||||
- Keep this slice about state ownership, not feature behavior.
|
||||
- Prefer moving state to shell-local structs or service singletons owned by the
|
||||
platform target.
|
||||
|
||||
### Bundle 5 - Retire The Thick Workflow Bridges
|
||||
|
||||
Priority: `P2`
|
||||
|
||||
Why this bundle is later:
|
||||
These bridges still matter, but many recent slices spent too much effort
|
||||
polishing adapters without changing the bulk shape of the live app. This bundle
|
||||
stays active only after the main hotspots are moving.
|
||||
|
||||
#### ARC-WKF-001 - Thin Document Open/Save/Session Bridges To Pure Adapters
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
The document session/open/save planners exist, but the live bridges still own a
|
||||
lot of retained dialog, metadata, title, and save execution behavior.
|
||||
|
||||
Write scope:
|
||||
- `src/legacy_document_open_services.*`
|
||||
- `src/legacy_document_session_services.*`
|
||||
- `src/legacy_document_export_services.*`
|
||||
- `src/legacy_history_services.*`
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/document_route.h`
|
||||
- `src/app_core/document_session.h`
|
||||
- `src/app_core/document_export.h`
|
||||
|
||||
Done when:
|
||||
- The remaining bridge files are thin adapters from planner outputs to retained
|
||||
execution.
|
||||
- Save/open/session flows stop mutating app/document/UI state inline across
|
||||
multiple bridge layers.
|
||||
- Title updates, history clearing, overwrite prompts, and save routing are each
|
||||
owned in one obvious place.
|
||||
|
||||
Mini-model packet:
|
||||
- Preserve current planner contracts.
|
||||
- Favor one adapter per workflow family over catch-all helper growth.
|
||||
|
||||
#### ARC-WKF-002 - Split Cloud And Brush Package Work Out Of Retained UI Nodes
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Cloud browse/download/upload and brush package import/export still close over
|
||||
retained nodes, worker threads, and direct UI ownership.
|
||||
|
||||
Write scope:
|
||||
- `src/legacy_cloud_services.*`
|
||||
- `src/node_dialog_cloud.*`
|
||||
- `src/legacy_brush_package_import_services.*`
|
||||
- `src/legacy_brush_package_export_services.*`
|
||||
- `src/node_panel_brush.cpp`
|
||||
|
||||
Read scope:
|
||||
- `src/app_core/document_cloud.h`
|
||||
- `src/app_core/brush_package_import.h`
|
||||
- `src/app_core/brush_package_export.h`
|
||||
- `src/assets/brush_package.*`
|
||||
|
||||
Done when:
|
||||
- Network transfer execution, thumbnail loading, and brush package worker
|
||||
ownership are isolated behind named services.
|
||||
- Retained nodes become view/controller shells instead of workflow owners.
|
||||
- Cloud and brush package code no longer need to be understood through panel or
|
||||
dialog internals first.
|
||||
|
||||
Mini-model packet:
|
||||
- Split worker ownership from UI ownership first.
|
||||
- Do not try to redesign cloud UX or brush preset UX in the same slice.
|
||||
|
||||
## Deferred On Purpose
|
||||
|
||||
- Vulkan and Metal lab work
|
||||
- package-only and automation-only cleanup
|
||||
- scorekeeping tasks that do not move app architecture
|
||||
|
||||
These remain in history only until the app shell, platform split, UI split, and
|
||||
canvas/render split are materially thinner.
|
||||
@@ -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"
|
||||
513
scripts/automation/package-smoke.ps1
Normal file
513
scripts/automation/package-smoke.ps1
Normal file
@@ -0,0 +1,513 @@
|
||||
[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 Resolve-PackageStatus {
|
||||
param(
|
||||
[bool]$RootCMakePackageTargetAvailable,
|
||||
[bool]$GateBlocked,
|
||||
[object[]]$Prerequisites
|
||||
)
|
||||
|
||||
if ($GateBlocked) {
|
||||
return "blocked"
|
||||
}
|
||||
|
||||
foreach ($prerequisite in $Prerequisites) {
|
||||
if ($prerequisite.name -eq "root-cmake-package-target") {
|
||||
continue
|
||||
}
|
||||
if (-not $prerequisite.available) {
|
||||
return "blocked"
|
||||
}
|
||||
}
|
||||
|
||||
if ($RootCMakePackageTargetAvailable) {
|
||||
return "validated"
|
||||
}
|
||||
|
||||
return "compile-only"
|
||||
}
|
||||
|
||||
function Get-AndroidNativeCheckInfo {
|
||||
param(
|
||||
[string]$Kind,
|
||||
[bool]$AndroidNativeChecks,
|
||||
[object]$AndroidNativeValidation
|
||||
)
|
||||
|
||||
$command = switch ($Kind) {
|
||||
"android-standard-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard" }
|
||||
"android-quest-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly" }
|
||||
"android-focus-apk" { "powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly" }
|
||||
default { "" }
|
||||
}
|
||||
|
||||
if (-not $AndroidNativeChecks) {
|
||||
return @{ Available = $true; Detail = "$command (not run)" }
|
||||
}
|
||||
|
||||
if ($command.Length -eq 0) {
|
||||
return @{ Available = $false; Detail = "No Android native check plan for kind '$Kind'" }
|
||||
}
|
||||
|
||||
$packages = switch ($Kind) {
|
||||
"android-standard-apk" { @("standard") }
|
||||
"android-quest-apk" { @("quest") }
|
||||
"android-focus-apk" { @("focus") }
|
||||
default { @() }
|
||||
}
|
||||
|
||||
$result = $null
|
||||
foreach ($entry in $AndroidNativeValidation.results) {
|
||||
$hasAll = $true
|
||||
foreach ($pkg in $packages) {
|
||||
if (-not ($entry.packages -contains $pkg)) {
|
||||
$hasAll = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasAll) {
|
||||
$result = $entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $result) {
|
||||
return @{ Available = $false; Detail = "$command (not executed)" }
|
||||
}
|
||||
|
||||
if ($result.exitCode -ne 0) {
|
||||
return @{ Available = $false; Detail = "$command (exit $($result.exitCode))" }
|
||||
}
|
||||
|
||||
return @{ Available = $true; Detail = $command }
|
||||
}
|
||||
|
||||
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"
|
||||
$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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $true -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
|
||||
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
|
||||
-Prerequisites $prerequisites `
|
||||
-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"
|
||||
$androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation
|
||||
$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 $androidNativeCheck.Available -Detail $androidNativeCheck.Detail),
|
||||
(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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/android assembleDebug" `
|
||||
-Prerequisites $prerequisites `
|
||||
-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"
|
||||
$androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation
|
||||
$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 $androidNativeCheck.Available -Detail $androidNativeCheck.Detail),
|
||||
(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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/quest assembleDebug" `
|
||||
-Prerequisites $prerequisites `
|
||||
-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"
|
||||
$androidNativeCheck = Get-AndroidNativeCheckInfo -Kind $kind -AndroidNativeChecks $AndroidNativeChecks -AndroidNativeValidation $androidNativeValidation
|
||||
$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 $androidNativeCheck.Available -Detail $androidNativeCheck.Detail),
|
||||
(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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/focus assembleDebug" `
|
||||
-Prerequisites $prerequisites `
|
||||
-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"
|
||||
$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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $true -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
|
||||
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
|
||||
-Prerequisites $prerequisites `
|
||||
-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"
|
||||
$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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-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 $prerequisites `
|
||||
-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"
|
||||
$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")
|
||||
)
|
||||
$status = Resolve-PackageStatus -RootCMakePackageTargetAvailable $false -GateBlocked $false -Prerequisites $prerequisites
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status $status `
|
||||
-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 $prerequisites `
|
||||
-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
|
||||
656
scripts/automation/package-smoke.sh
Normal file
656
scripts/automation/package-smoke.sh
Normal file
@@ -0,0 +1,656 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="linux-clang"
|
||||
configuration="Debug"
|
||||
target="PanoPainter"
|
||||
cmake_command="cmake"
|
||||
artifact="out/build/$preset/$target"
|
||||
readiness_only=0
|
||||
android_native_checks=0
|
||||
package_kinds="windows-appx,android-standard-apk,android-quest-apk,android-focus-apk,apple-bundle,linux-app,webgl"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--readiness-only)
|
||||
readiness_only=1
|
||||
shift
|
||||
;;
|
||||
--android-native-checks)
|
||||
android_native_checks=1
|
||||
shift
|
||||
;;
|
||||
--package-kinds=*)
|
||||
package_kinds="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--package-kinds)
|
||||
shift
|
||||
if [ "$#" -gt 0 ]; then
|
||||
package_kinds="$1"
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$#" -ge 1 ]; then
|
||||
preset="${1:-$preset}"
|
||||
configuration="${2:-$configuration}"
|
||||
target="${3:-$target}"
|
||||
artifact="${4:-out/build/$preset/$target}"
|
||||
fi
|
||||
|
||||
start="$(date +%s)"
|
||||
root="$(pwd)"
|
||||
package_kinds="$(printf "%s" "$package_kinds" | tr -d " ")"
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r/\\r/g; s/\n/\\n/g'
|
||||
}
|
||||
|
||||
json_string() {
|
||||
printf '"%s"' "$(json_escape "$1")"
|
||||
}
|
||||
|
||||
json_bool() {
|
||||
if [ "$1" -eq 1 ]; then
|
||||
printf true
|
||||
else
|
||||
printf false
|
||||
fi
|
||||
}
|
||||
|
||||
json_array_from_csv() {
|
||||
local csv="$1"
|
||||
local first=1
|
||||
local value
|
||||
local items=""
|
||||
|
||||
IFS=','
|
||||
for value in $csv; do
|
||||
if [ -z "$value" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$first" -eq 1 ]; then
|
||||
first=0
|
||||
else
|
||||
items="${items},"
|
||||
fi
|
||||
items="${items}$(json_string "$value")"
|
||||
done
|
||||
unset IFS
|
||||
|
||||
printf "[%s]" "$items"
|
||||
}
|
||||
|
||||
command_available() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
file_available() {
|
||||
[ -f "$1" ]
|
||||
}
|
||||
|
||||
dir_available() {
|
||||
[ -d "$1" ]
|
||||
}
|
||||
|
||||
resolve_status() {
|
||||
local root_target="$1"
|
||||
local gate_blocked="$2"
|
||||
shift 2
|
||||
|
||||
if [ "$gate_blocked" -ne 0 ]; then
|
||||
printf "blocked"
|
||||
return
|
||||
fi
|
||||
|
||||
for prereq in "$@"; do
|
||||
if [ "$prereq" -ne 1 ]; then
|
||||
printf "blocked"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$root_target" -ne 0 ]; then
|
||||
printf "validated"
|
||||
else
|
||||
printf "compile-only"
|
||||
fi
|
||||
}
|
||||
|
||||
append_json_item() {
|
||||
if [ -z "$package_readiness" ]; then
|
||||
package_readiness="$1"
|
||||
else
|
||||
package_readiness="${package_readiness},$1"
|
||||
fi
|
||||
}
|
||||
|
||||
append_result_item() {
|
||||
if [ -z "$android_native_results" ]; then
|
||||
android_native_results="$1"
|
||||
else
|
||||
android_native_results="${android_native_results},$1"
|
||||
fi
|
||||
}
|
||||
|
||||
prerequisite_entry() {
|
||||
local name="$1"
|
||||
local available="$2"
|
||||
local detail="$3"
|
||||
|
||||
printf '{'
|
||||
printf '"name":%s,' "$(json_string "$name")"
|
||||
printf '"available":%s,' "$(json_bool "$available")"
|
||||
printf '"detail":%s' "$(json_string "$detail")"
|
||||
printf '}'
|
||||
}
|
||||
|
||||
artifact_entry() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
local path_type="$3"
|
||||
local exists=0
|
||||
|
||||
printf '{'
|
||||
printf '"name":%s,' "$(json_string "$name")"
|
||||
printf '"path":%s,' "$(json_string "$path")"
|
||||
printf '"pathType":%s,' "$(json_string "$path_type")"
|
||||
if [ "$path_type" = "Leaf" ]; then
|
||||
[ -f "$path" ]
|
||||
exists=$([ $? -eq 0 ] && printf "1" || printf "0")
|
||||
elif [ "$path_type" = "Container" ]; then
|
||||
[ -d "$path" ]
|
||||
exists=$([ $? -eq 0 ] && printf "1" || printf "0")
|
||||
else
|
||||
[ -e "$path" ]
|
||||
exists=$([ $? -eq 0 ] && printf "1" || printf "0")
|
||||
fi
|
||||
printf '"exists":%s' "$(json_bool "$exists")"
|
||||
printf '}'
|
||||
}
|
||||
|
||||
check_entry() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
local exists="$3"
|
||||
|
||||
printf '{'
|
||||
printf '"name":%s,' "$(json_string "$name")"
|
||||
printf '"path":%s,' "$(json_string "$path")"
|
||||
printf '"exists":%s' "$(json_bool "$exists")"
|
||||
printf '}'
|
||||
}
|
||||
|
||||
is_kind_requested() {
|
||||
case ",${package_kinds}," in
|
||||
*,"$1",*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_android_native_check() {
|
||||
local packages="$1"
|
||||
local configure_only="$2"
|
||||
local command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $packages"
|
||||
if [ "$configure_only" -ne 0 ]; then
|
||||
command="${command} -ConfigureOnly"
|
||||
fi
|
||||
|
||||
if ! command_available powershell; then
|
||||
android_native_last_exit_code=127
|
||||
printf '{"packages":%s,"configureOnly":%s,"exitCode":%s,"command":%s,"summary":null}' \
|
||||
"$(json_array_from_csv "$packages")" \
|
||||
"$(json_bool "$configure_only")" \
|
||||
"$android_native_last_exit_code" \
|
||||
"$(json_string "$command")"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$configure_only" -ne 0 ]; then
|
||||
output="$(powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages "$packages" -ConfigureOnly 2>&1)"
|
||||
else
|
||||
output="$(powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages "$packages" 2>&1)"
|
||||
fi
|
||||
android_native_last_exit_code=$?
|
||||
|
||||
summary="$(printf '%s\n' "$output" | awk 'BEGIN{line="";} /^\{/{line=$0} END{if (line != "") print line}')"
|
||||
if [ -z "$summary" ]; then
|
||||
summary="null"
|
||||
fi
|
||||
|
||||
printf '{"packages":%s,"configureOnly":%s,"exitCode":%s,"command":%s,"summary":%s}' \
|
||||
"$(json_array_from_csv "$packages")" \
|
||||
"$(json_bool "$configure_only")" \
|
||||
"$android_native_last_exit_code" \
|
||||
"$(json_string "$command")" \
|
||||
"$summary"
|
||||
}
|
||||
|
||||
extract_exit_code() {
|
||||
printf '%s' "$1" | awk -F '"exitCode":' 'NF == 2 { gsub(/[^0-9].*/, "", $2); print $2 }'
|
||||
}
|
||||
|
||||
build_android_native_validation() {
|
||||
local standard_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard"
|
||||
local qf_packages=""
|
||||
local qf_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $qf_packages -ConfigureOnly"
|
||||
|
||||
local request_standard=0
|
||||
local request_qf=0
|
||||
|
||||
if is_kind_requested "android-standard-apk"; then
|
||||
request_standard=1
|
||||
fi
|
||||
if is_kind_requested "android-quest-apk" || is_kind_requested "android-focus-apk"; then
|
||||
request_qf=1
|
||||
if is_kind_requested "android-quest-apk"; then
|
||||
qf_packages="quest"
|
||||
fi
|
||||
if is_kind_requested "android-focus-apk"; then
|
||||
if [ -n "$qf_packages" ]; then
|
||||
qf_packages="${qf_packages},focus"
|
||||
else
|
||||
qf_packages="focus"
|
||||
fi
|
||||
fi
|
||||
qf_command="powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages $qf_packages -ConfigureOnly"
|
||||
fi
|
||||
|
||||
android_native_standard_available=1
|
||||
android_native_quest_available=1
|
||||
android_native_focus_available=1
|
||||
android_native_standard_detail="${standard_command} (not run)"
|
||||
android_native_quest_detail="${qf_command} (not run)"
|
||||
android_native_focus_detail="${qf_command} (not run)"
|
||||
|
||||
local requested="false"
|
||||
local exit_code=0
|
||||
android_native_results=""
|
||||
android_native_last_exit_code=0
|
||||
|
||||
if [ "$android_native_checks" -eq 0 ]; then
|
||||
android_native_validation='{"requested":false,"exitCode":0,"results":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$request_standard" -eq 1 ]; then
|
||||
requested="true"
|
||||
standard_result="$(run_android_native_check "standard" 0)"
|
||||
append_result_item "$standard_result"
|
||||
standard_exit_code="$(extract_exit_code "$standard_result")"
|
||||
if [ -z "$standard_exit_code" ]; then
|
||||
standard_exit_code=0
|
||||
fi
|
||||
if [ "$standard_exit_code" -ne 0 ] && [ "$exit_code" -eq 0 ]; then
|
||||
exit_code="$standard_exit_code"
|
||||
fi
|
||||
|
||||
if [ "$standard_exit_code" -eq 0 ]; then
|
||||
android_native_standard_available=1
|
||||
android_native_standard_detail="$standard_command"
|
||||
else
|
||||
android_native_standard_available=0
|
||||
android_native_standard_detail="${standard_command} (exit $standard_exit_code)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$request_qf" -eq 1 ] && [ -n "$qf_packages" ]; then
|
||||
requested="true"
|
||||
qf_result="$(run_android_native_check "$qf_packages" 1)"
|
||||
append_result_item "$qf_result"
|
||||
qf_exit_code="$(extract_exit_code "$qf_result")"
|
||||
if [ -z "$qf_exit_code" ]; then
|
||||
qf_exit_code=0
|
||||
fi
|
||||
if [ "$qf_exit_code" -ne 0 ] && [ "$exit_code" -eq 0 ]; then
|
||||
exit_code="$qf_exit_code"
|
||||
fi
|
||||
|
||||
if [ "$qf_exit_code" -eq 0 ]; then
|
||||
if is_kind_requested "android-quest-apk"; then
|
||||
android_native_quest_available=1
|
||||
android_native_quest_detail="$qf_command"
|
||||
else
|
||||
android_native_quest_detail="$qf_command (not executed)"
|
||||
fi
|
||||
|
||||
if is_kind_requested "android-focus-apk"; then
|
||||
android_native_focus_available=1
|
||||
android_native_focus_detail="$qf_command"
|
||||
else
|
||||
android_native_focus_detail="$qf_command (not executed)"
|
||||
fi
|
||||
else
|
||||
if is_kind_requested "android-quest-apk"; then
|
||||
android_native_quest_available=0
|
||||
android_native_quest_detail="${qf_command} (exit $qf_exit_code)"
|
||||
else
|
||||
android_native_quest_detail="$qf_command (not executed)"
|
||||
fi
|
||||
|
||||
if is_kind_requested "android-focus-apk"; then
|
||||
android_native_focus_available=0
|
||||
android_native_focus_detail="${qf_command} (exit $qf_exit_code)"
|
||||
else
|
||||
android_native_focus_detail="$qf_command (not executed)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
android_native_validation="{\"requested\":$requested,\"exitCode\":$exit_code,\"results\":[${android_native_results}]}"
|
||||
}
|
||||
|
||||
build_package_readiness() {
|
||||
package_readiness=""
|
||||
|
||||
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")
|
||||
|
||||
if is_kind_requested "windows-appx"; then
|
||||
windows_status="$(resolve_status 0 1 "$windows_wapproj_exists" "$windows_manifest_exists" "$makeappx_exists" "$signtool_exists")"
|
||||
windows_prerequisites=''
|
||||
windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "legacy-wapproj" "$windows_wapproj_exists" "$windows_wapproj"),"
|
||||
windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "appx-manifest" "$windows_manifest_exists" "$windows_manifest"),"
|
||||
windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "makeappx" "$makeappx_exists" "Windows SDK packaging tool"),"
|
||||
windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "signtool" "$signtool_exists" "Windows SDK signing tool"),"
|
||||
windows_prerequisites="${windows_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
windows_entry="{"
|
||||
windows_entry="${windows_entry}\"kind\":\"windows-appx\","
|
||||
windows_entry="${windows_entry}\"status\":\"$windows_status\","
|
||||
windows_entry="${windows_entry}\"reason\":\"legacy-wapproj-present-but-root-cmake-package-target-missing\","
|
||||
windows_entry="${windows_entry}\"debt\":\"DEBT-0011\","
|
||||
windows_entry="${windows_entry}\"validationCommand\":\"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$configuration /p:Platform=x64\","
|
||||
windows_entry="${windows_entry}\"prerequisites\":[${windows_prerequisites}],"
|
||||
windows_entry="${windows_entry}\"artifacts\":["
|
||||
windows_entry="${windows_entry}$(artifact_entry "app-packages" "$windows_output" "Container")"
|
||||
windows_entry="${windows_entry}]}"
|
||||
append_json_item "$windows_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "android-standard-apk"; then
|
||||
android_standard_status="$(resolve_status 0 0 "$android_standard_gradle_exists" "$android_standard_manifest_exists" "$gradle_exists" "$android_native_standard_available")"
|
||||
android_standard_prerequisites=''
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "gradle-build" "$android_standard_gradle_exists" "$android_standard_gradle"),"
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "android-manifest" "$android_standard_manifest_exists" "$android_standard_manifest"),"
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder"),"
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_standard_available" "$android_native_standard_detail"),"
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-arm64/android-x64"),"
|
||||
android_standard_prerequisites="${android_standard_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
android_standard_entry="{"
|
||||
android_standard_entry="${android_standard_entry}\"kind\":\"android-standard-apk\","
|
||||
android_standard_entry="${android_standard_entry}\"status\":\"$android_standard_status\","
|
||||
android_standard_entry="${android_standard_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\","
|
||||
android_standard_entry="${android_standard_entry}\"debt\":\"DEBT-0011\","
|
||||
android_standard_entry="${android_standard_entry}\"validationCommand\":\"gradle -p android/android assembleDebug\","
|
||||
android_standard_entry="${android_standard_entry}\"prerequisites\":[${android_standard_prerequisites}],"
|
||||
android_standard_entry="${android_standard_entry}\"artifacts\":["
|
||||
android_standard_entry="${android_standard_entry}$(artifact_entry "apk-output" "$android_standard_output" "Container")"
|
||||
android_standard_entry="${android_standard_entry}]}"
|
||||
append_json_item "$android_standard_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "android-quest-apk"; then
|
||||
android_quest_status="$(resolve_status 0 0 "$android_quest_gradle_exists" "$android_quest_manifest_exists" "$gradle_exists" "$android_native_quest_available")"
|
||||
android_quest_prerequisites=''
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "gradle-build" "$android_quest_gradle_exists" "$android_quest_gradle"),"
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "android-manifest" "$android_quest_manifest_exists" "$android_quest_manifest"),"
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder"),"
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_quest_available" "$android_native_quest_detail"),"
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-quest-arm64"),"
|
||||
android_quest_prerequisites="${android_quest_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
android_quest_entry="{"
|
||||
android_quest_entry="${android_quest_entry}\"kind\":\"android-quest-apk\","
|
||||
android_quest_entry="${android_quest_entry}\"status\":\"$android_quest_status\","
|
||||
android_quest_entry="${android_quest_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\","
|
||||
android_quest_entry="${android_quest_entry}\"debt\":\"DEBT-0011\","
|
||||
android_quest_entry="${android_quest_entry}\"validationCommand\":\"gradle -p android/quest assembleDebug\","
|
||||
android_quest_entry="${android_quest_entry}\"prerequisites\":[${android_quest_prerequisites}],"
|
||||
android_quest_entry="${android_quest_entry}\"artifacts\":["
|
||||
android_quest_entry="${android_quest_entry}$(artifact_entry "apk-output" "$android_quest_output" "Container")"
|
||||
android_quest_entry="${android_quest_entry}]}"
|
||||
append_json_item "$android_quest_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "android-focus-apk"; then
|
||||
android_focus_status="$(resolve_status 0 0 "$android_focus_gradle_exists" "$android_focus_manifest_exists" "$gradle_exists" "$android_native_focus_available")"
|
||||
android_focus_prerequisites=''
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "gradle-build" "$android_focus_gradle_exists" "$android_focus_gradle"),"
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "android-manifest" "$android_focus_manifest_exists" "$android_focus_manifest"),"
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "gradle" "$gradle_exists" "Android package builder"),"
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "retained-native-cmake-check" "$android_native_focus_available" "$android_native_focus_detail"),"
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "android-focus-arm64"),"
|
||||
android_focus_prerequisites="${android_focus_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
android_focus_entry="{"
|
||||
android_focus_entry="${android_focus_entry}\"kind\":\"android-focus-apk\","
|
||||
android_focus_entry="${android_focus_entry}\"status\":\"$android_focus_status\","
|
||||
android_focus_entry="${android_focus_entry}\"reason\":\"legacy-gradle-package-not-consuming-root-cmake-targets\","
|
||||
android_focus_entry="${android_focus_entry}\"debt\":\"DEBT-0011\","
|
||||
android_focus_entry="${android_focus_entry}\"validationCommand\":\"gradle -p android/focus assembleDebug\","
|
||||
android_focus_entry="${android_focus_entry}\"prerequisites\":[${android_focus_prerequisites}],"
|
||||
android_focus_entry="${android_focus_entry}\"artifacts\":["
|
||||
android_focus_entry="${android_focus_entry}$(artifact_entry "apk-output" "$android_focus_output" "Container")"
|
||||
android_focus_entry="${android_focus_entry}]}"
|
||||
append_json_item "$android_focus_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "apple-bundle"; then
|
||||
apple_status="$(resolve_status 0 1 "$apple_project_exists" "$xcodebuild_exists")"
|
||||
apple_prerequisites=''
|
||||
apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "legacy-xcode-project" "$apple_project_exists" "$apple_project"),"
|
||||
apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "xcodebuild" "$xcodebuild_exists" "Apple package builder"),"
|
||||
apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "macos/ios-device/ios-simulator"),"
|
||||
apple_prerequisites="${apple_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
apple_entry="{"
|
||||
apple_entry="${apple_entry}\"kind\":\"apple-bundle\","
|
||||
apple_entry="${apple_entry}\"status\":\"$apple_status\","
|
||||
apple_entry="${apple_entry}\"reason\":\"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target\","
|
||||
apple_entry="${apple_entry}\"debt\":\"DEBT-0011\","
|
||||
apple_entry="${apple_entry}\"validationCommand\":\"xcodebuild -project PanoPainter.xcodeproj -configuration $configuration\","
|
||||
apple_entry="${apple_entry}\"prerequisites\":[${apple_prerequisites}],"
|
||||
apple_entry="${apple_entry}\"artifacts\":["
|
||||
apple_entry="${apple_entry}$(artifact_entry "apple-package-output" "$apple_output" "Container")"
|
||||
apple_entry="${apple_entry}]}"
|
||||
append_json_item "$apple_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "linux-app"; then
|
||||
linux_status="$(resolve_status 0 0 "$linux_cmake_exists" "$cmake_exists")"
|
||||
linux_prerequisites=''
|
||||
linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "retained-linux-cmake" "$linux_cmake_exists" "$linux_cmake"),"
|
||||
linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "cmake" "$cmake_exists" "Linux retained app CMake configure/build tool"),"
|
||||
linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "retained-platform-cmake-baseline" 1 "python scripts/dev/check_retained_platform_cmake.py"),"
|
||||
linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "linux-clang"),"
|
||||
linux_prerequisites="${linux_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
linux_entry="{"
|
||||
linux_entry="${linux_entry}\"kind\":\"linux-app\","
|
||||
linux_entry="${linux_entry}\"status\":\"$linux_status\","
|
||||
linux_entry="${linux_entry}\"reason\":\"retained-linux-cmake-not-consuming-root-cmake-targets\","
|
||||
linux_entry="${linux_entry}\"debt\":\"DEBT-0011\","
|
||||
linux_entry="${linux_entry}\"validationCommand\":\"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter\","
|
||||
linux_entry="${linux_entry}\"prerequisites\":[${linux_prerequisites}],"
|
||||
linux_entry="${linux_entry}\"artifacts\":["
|
||||
linux_entry="${linux_entry}$(artifact_entry "linux-app-output" "$linux_output" "Leaf")"
|
||||
linux_entry="${linux_entry}]}"
|
||||
append_json_item "$linux_entry"
|
||||
fi
|
||||
|
||||
if is_kind_requested "webgl"; then
|
||||
webgl_status="$(resolve_status 0 0 "$webgl_cmake_exists" "$emcc_exists" "$emcmake_exists")"
|
||||
webgl_prerequisites=''
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "retained-webgl-cmake" "$webgl_cmake_exists" "$webgl_cmake"),"
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "emcc" "$emcc_exists" "Emscripten compiler"),"
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "emcmake" "$emcmake_exists" "Emscripten CMake wrapper"),"
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "retained-platform-cmake-baseline" 1 "python scripts/dev/check_retained_platform_cmake.py"),"
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "root-cmake-preset" 1 "emscripten"),"
|
||||
webgl_prerequisites="${webgl_prerequisites}$(prerequisite_entry "root-cmake-package-target" 0 "Not migrated yet")"
|
||||
webgl_entry="{"
|
||||
webgl_entry="${webgl_entry}\"kind\":\"webgl\","
|
||||
webgl_entry="${webgl_entry}\"status\":\"$webgl_status\","
|
||||
webgl_entry="${webgl_entry}\"reason\":\"retained-webgl-cmake-not-consuming-root-cmake-targets\","
|
||||
webgl_entry="${webgl_entry}\"debt\":\"DEBT-0011\","
|
||||
webgl_entry="${webgl_entry}\"validationCommand\":\"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter\","
|
||||
webgl_entry="${webgl_entry}\"prerequisites\":[${webgl_prerequisites}],"
|
||||
webgl_entry="${webgl_entry}\"artifacts\":["
|
||||
webgl_entry="${webgl_entry}$(artifact_entry "webgl-output" "$webgl_output" "Container")"
|
||||
webgl_entry="${webgl_entry}]}"
|
||||
append_json_item "$webgl_entry"
|
||||
fi
|
||||
|
||||
printf "[%s]" "$package_readiness"
|
||||
}
|
||||
|
||||
build_android_native_validation
|
||||
|
||||
if [ "$readiness_only" -eq 1 ]; then
|
||||
elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))"
|
||||
package_readiness="$(build_package_readiness)"
|
||||
printf '{"command":"package-smoke",'
|
||||
printf '"preset":%s,"configuration":%s,"target":%s,' \
|
||||
"$(json_string "$preset")" \
|
||||
"$(json_string "$configuration")" \
|
||||
"$(json_string "$target")"
|
||||
printf '"stage":"readiness","exitCode":0,"elapsedMs":%s,' "$elapsed_ms"
|
||||
printf '"androidNativeValidation":%s,' "$android_native_validation"
|
||||
printf '"packageReadiness":%s}\n' "$package_readiness"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
$cmake_command --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
build_exit="$?"
|
||||
if [ "$build_exit" -ne 0 ]; then
|
||||
elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))"
|
||||
package_readiness="$(build_package_readiness)"
|
||||
printf '{"command":"package-smoke",'
|
||||
printf '"preset":%s,"configuration":%s,"target":%s,' \
|
||||
"$(json_string "$preset")" \
|
||||
"$(json_string "$configuration")" \
|
||||
"$(json_string "$target")"
|
||||
printf '"cmakeCommand":%s,' "$(json_string "$cmake_command")"
|
||||
printf '"stage":"build","exitCode":%s,"elapsedMs":%s,' "$build_exit" "$elapsed_ms"
|
||||
printf '"androidNativeValidation":%s,' "$android_native_validation"
|
||||
printf '"packageReadiness":%s}\n' "$package_readiness"
|
||||
exit "$build_exit"
|
||||
fi
|
||||
|
||||
binary="${root}/out/build/$preset/$configuration/$target.exe"
|
||||
binary_dir="$(printf '%s' "$binary" | sed 's#/[^/]*$##')"
|
||||
data_dir="$binary_dir/data"
|
||||
curl_dll="$(
|
||||
if [ "$configuration" = "Debug" ]; then
|
||||
printf "libcurl_debug.dll"
|
||||
else
|
||||
printf "libcurl.dll"
|
||||
fi
|
||||
)"
|
||||
|
||||
checks=''
|
||||
checks="${checks}$(check_entry "executable" "$binary" "$([ -f "$binary" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "data" "$data_dir" "$([ -d "$data_dir" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "BugTrapU-x64.dll" "$binary_dir/BugTrapU-x64.dll" "$([ -f "$binary_dir/BugTrapU-x64.dll" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "$curl_dll" "$binary_dir/$curl_dll" "$([ -f "$binary_dir/$curl_dll" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "libyuv.dll" "$binary_dir/libyuv.dll" "$([ -f "$binary_dir/libyuv.dll" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "libmp4v2.dll" "$binary_dir/libmp4v2.dll" "$([ -f "$binary_dir/libmp4v2.dll" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "openh264-2.0.0-win64.dll" "$binary_dir/openh264-2.0.0-win64.dll" "$([ -f "$binary_dir/openh264-2.0.0-win64.dll" ] && printf "1" || printf "0")"),"
|
||||
checks="${checks}$(check_entry "openvr_api.dll" "$binary_dir/openvr_api.dll" "$([ -f "$binary_dir/openvr_api.dll" ] && printf "1" || printf "0")")"
|
||||
|
||||
artifact_exists="$( [ -e "$artifact" ] && printf 1 || printf 0 )"
|
||||
exit_code=0
|
||||
if [ "$artifact_exists" -eq 0 ]; then
|
||||
exit_code=2
|
||||
fi
|
||||
if [ "$android_native_checks" -ne 0 ] && [ "$exit_code" -eq 0 ]; then
|
||||
exit_code="$(echo "$android_native_validation" | sed -n 's/.*"exitCode":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')"
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
exit_code=0
|
||||
else
|
||||
exit_code=1
|
||||
fi
|
||||
fi
|
||||
|
||||
package_readiness="$(build_package_readiness)"
|
||||
elapsed_ms="$(( ( $(date +%s) - start ) * 1000 ))"
|
||||
|
||||
printf '{"command":"package-smoke",'
|
||||
printf '"preset":%s,"configuration":%s,"target":%s,' \
|
||||
"$(json_string "$preset")" \
|
||||
"$(json_string "$configuration")" \
|
||||
"$(json_string "$target")"
|
||||
printf '"artifact":%s,"exists":%s,"cmakeCommand":%s,' \
|
||||
"$(json_string "$artifact")" \
|
||||
"$(json_bool "$artifact_exists")" \
|
||||
"$(json_string "$cmake_command")"
|
||||
printf '"exitCode":%s,"elapsedMs":%s,' "$exit_code" "$elapsed_ms"
|
||||
printf '"checks":[%s],' "$checks"
|
||||
printf '"androidNativeValidation":%s,' "$android_native_validation"
|
||||
printf '"packageReadiness":%s}\n' "$package_readiness"
|
||||
exit "$exit_code"
|
||||
204
scripts/automation/platform-build.ps1
Normal file
204
scripts/automation/platform-build.ps1
Normal file
@@ -0,0 +1,204 @@
|
||||
[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_foundation_task_queue_stress_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_paint_renderer_stroke_execution_tests",
|
||||
"pp_renderer_gl_gpu_readback_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_app_thread_stress_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_task_queue_stress_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_paint_renderer_stroke_execution_tests pp_renderer_gl_gpu_readback_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_thread_stress_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"
|
||||
246
scripts/dev/check_component_boundaries.py
Normal file
246
scripts/dev/check_component_boundaries.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate component boundary rules for pure architectural targets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""")
|
||||
SINGLETON_RE = re.compile(r"\b(?:App::I|Canvas::I)\b")
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC_ROOT = REPO_ROOT / "src"
|
||||
CMAKE_FILE = REPO_ROOT / "CMakeLists.txt"
|
||||
|
||||
COMPONENT_BY_DIR = {
|
||||
"foundation": "pp_foundation",
|
||||
"assets": "pp_assets",
|
||||
"paint": "pp_paint",
|
||||
"document": "pp_document",
|
||||
"renderer_api": "pp_renderer_api",
|
||||
"paint_renderer": "pp_paint_renderer",
|
||||
"ui_core": "pp_ui_core",
|
||||
"app_core": "pp_app_core",
|
||||
}
|
||||
|
||||
PURE_TARGETS = set(COMPONENT_BY_DIR.values())
|
||||
TARGET_INFRA = {"pp_project_options", "pp_project_warnings", "pp_xml_tinyxml2"}
|
||||
|
||||
ALLOWED_LINKS = {
|
||||
"pp_foundation": set(),
|
||||
"pp_assets": {"pp_foundation"},
|
||||
"pp_paint": {"pp_foundation"},
|
||||
"pp_document": {"pp_foundation", "pp_assets", "pp_paint"},
|
||||
"pp_renderer_api": {"pp_foundation"},
|
||||
"pp_paint_renderer": {"pp_foundation", "pp_paint", "pp_document", "pp_renderer_api"},
|
||||
"pp_ui_core": {"pp_foundation", "pp_xml_tinyxml2"},
|
||||
"pp_app_core": {"pp_foundation", "pp_document", "pp_assets", "pp_paint", "pp_ui_core"},
|
||||
}
|
||||
|
||||
ALLOWED_LOCAL_INCLUDES = {
|
||||
"pp_foundation": ("foundation/",),
|
||||
"pp_assets": ("foundation/", "assets/"),
|
||||
"pp_paint": ("foundation/", "paint/"),
|
||||
"pp_document": ("foundation/", "assets/", "paint/", "document/"),
|
||||
"pp_renderer_api": ("foundation/", "renderer_api/"),
|
||||
"pp_paint_renderer": (
|
||||
"assets/",
|
||||
"document/",
|
||||
"foundation/",
|
||||
"paint/",
|
||||
"paint_renderer/",
|
||||
"renderer_api/",
|
||||
),
|
||||
"pp_ui_core": ("foundation/", "ui_core/"),
|
||||
"pp_app_core": ("app_core/", "assets/", "document/", "foundation/", "paint/", "ui_core/"),
|
||||
}
|
||||
|
||||
ALLOWED_EXTERNAL_PREFIXES = (
|
||||
"stb/",
|
||||
)
|
||||
|
||||
FORBIDDEN_INCLUDE_TOKENS = (
|
||||
"platform/windows",
|
||||
"platform/windows",
|
||||
"platform_apple",
|
||||
"platform_legacy",
|
||||
"platform_api/",
|
||||
"platform_legacy/",
|
||||
"platform_apple/",
|
||||
"platform_windows/",
|
||||
"opengl/",
|
||||
"/opengl",
|
||||
"<gl",
|
||||
"glad/",
|
||||
"<GL/",
|
||||
"<openGL/",
|
||||
"vulkan/",
|
||||
"directx",
|
||||
"d3d",
|
||||
"android/",
|
||||
"ios/",
|
||||
"objc/",
|
||||
"metal/",
|
||||
"windows.h",
|
||||
"wingdi.h",
|
||||
"afxwin.h",
|
||||
"App::I",
|
||||
"Canvas::I",
|
||||
)
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return REPO_ROOT
|
||||
|
||||
|
||||
def component_for_path(path: Path) -> str | None:
|
||||
try:
|
||||
rel = path.relative_to(SRC_ROOT)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if not rel.parts:
|
||||
return None
|
||||
return COMPONENT_BY_DIR.get(rel.parts[0])
|
||||
|
||||
|
||||
def collect_target_links(cmake_text: str, target: str) -> list[str] | None:
|
||||
pattern = re.compile(rf"target_link_libraries\(\s*{re.escape(target)}\s+(.*?)\)", re.S | re.I)
|
||||
matches = pattern.findall(cmake_text)
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
tokens: list[str] = []
|
||||
for block in matches:
|
||||
block = block.replace("\\\n", " ")
|
||||
for token in re.split(r"\s+", block):
|
||||
token = token.strip()
|
||||
if not token or token.upper() in {"PUBLIC", "PRIVATE", "INTERFACE"}:
|
||||
continue
|
||||
token = token.strip("\"'")
|
||||
if token.startswith("$<") or token.startswith("SHELL:"):
|
||||
continue
|
||||
tokens.append(token)
|
||||
return tokens
|
||||
|
||||
|
||||
def check_link_dependencies(cmake_text: str) -> list[dict[str, Any]]:
|
||||
violations: list[dict[str, Any]] = []
|
||||
for target in sorted(PURE_TARGETS):
|
||||
deps = collect_target_links(cmake_text, target)
|
||||
if deps is None:
|
||||
violations.append(
|
||||
{
|
||||
"target": target,
|
||||
"dependency": None,
|
||||
"kind": "missing-target-link-declaration",
|
||||
"message": "No target_link_libraries block found",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
allowed = ALLOWED_LINKS[target]
|
||||
for dependency in deps:
|
||||
if dependency in TARGET_INFRA:
|
||||
continue
|
||||
if dependency in allowed:
|
||||
continue
|
||||
if dependency.startswith("pp_"):
|
||||
violations.append(
|
||||
{
|
||||
"target": target,
|
||||
"dependency": dependency,
|
||||
"kind": "invalid-target-edge",
|
||||
"message": f"{target} must not depend on {dependency}",
|
||||
}
|
||||
)
|
||||
return violations
|
||||
|
||||
|
||||
def is_forbidden_include(component: str, include: str) -> tuple[bool, str | None]:
|
||||
include_lower = include.lower()
|
||||
if any(token in include_lower for token in FORBIDDEN_INCLUDE_TOKENS):
|
||||
token = next(token for token in FORBIDDEN_INCLUDE_TOKENS if token in include_lower)
|
||||
return True, token
|
||||
if "/" in include_lower and any(include_lower.startswith(prefix) for prefix in ALLOWED_EXTERNAL_PREFIXES):
|
||||
return False, None
|
||||
|
||||
if "/" in include:
|
||||
allowed_prefixes = ALLOWED_LOCAL_INCLUDES[component]
|
||||
if not any(include_lower.startswith(prefix) for prefix in allowed_prefixes):
|
||||
return True, "component-boundary-crossing-include"
|
||||
return False, None
|
||||
|
||||
|
||||
def check_pure_component_sources() -> list[dict[str, Any]]:
|
||||
violations: list[dict[str, Any]] = []
|
||||
|
||||
for path in SRC_ROOT.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}:
|
||||
continue
|
||||
|
||||
component = component_for_path(path)
|
||||
if component is None:
|
||||
continue
|
||||
|
||||
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
include_match = INCLUDE_RE.match(line)
|
||||
if include_match:
|
||||
include = (include_match.group(2) or include_match.group(3) or "").strip()
|
||||
forbidden, reason = is_forbidden_include(component, include)
|
||||
if forbidden:
|
||||
violations.append(
|
||||
{
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": line_no,
|
||||
"kind": "forbidden-include",
|
||||
"include": include,
|
||||
"detail": reason,
|
||||
"text": line.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
if SINGLETON_RE.search(line):
|
||||
violations.append(
|
||||
{
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": line_no,
|
||||
"kind": "legacy-singleton-reference",
|
||||
"detail": "App::I/Canvas::I is not allowed in pure components",
|
||||
"text": line.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
source_violations = check_pure_component_sources()
|
||||
cmake_text = CMAKE_FILE.read_text(encoding="utf-8")
|
||||
link_violations = check_link_dependencies(cmake_text)
|
||||
|
||||
all_violations = source_violations + link_violations
|
||||
ok = len(all_violations) == 0
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": ok,
|
||||
"summary": {
|
||||
"sourceViolationCount": len(source_violations),
|
||||
"linkViolationCount": len(link_violations),
|
||||
},
|
||||
"violations": all_violations,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
181
scripts/dev/check_package_smoke_readiness.py
Normal file
181
scripts/dev/check_package_smoke_readiness.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/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_windows_appx_package_readiness",
|
||||
"panopainter_apple_bundle_package_readiness",
|
||||
"panopainter_android_standard_native_package",
|
||||
"panopainter_android_standard_apk_package_readiness",
|
||||
"panopainter_android_quest_apk_package_readiness",
|
||||
"panopainter_android_focus_apk_package_readiness",
|
||||
"panopainter_android_vr_native_package_configure",
|
||||
"panopainter_android_native_package_smoke",
|
||||
"panopainter_linux_app_package_readiness",
|
||||
"panopainter_webgl_package_readiness",
|
||||
"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")
|
||||
match = re.search(r'package_kinds="([^"]+)"', script)
|
||||
if match:
|
||||
return sorted(set(filter(None, (value.strip() for value in match.group(1).split(",")))))
|
||||
|
||||
quoted_kinds = sorted(set(re.findall(r'"kind":"([^"]+)"', script)))
|
||||
escaped_kinds = sorted(set(re.findall(r'\\"kind\\":\\"([^\\"]+)\\"', script)))
|
||||
if quoted_kinds or escaped_kinds:
|
||||
return sorted(set(quoted_kinds).union(escaped_kinds))
|
||||
raise RuntimeError("Could not find package kinds defaults in package-smoke.sh")
|
||||
|
||||
|
||||
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)
|
||||
script_texts = {
|
||||
"package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8"),
|
||||
"package-smoke.sh": (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8"),
|
||||
}
|
||||
debt_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r'debt\s*=\s*"DEBT-0011"',
|
||||
"package-smoke.sh": r'\\"debt\\":\\"DEBT-0011\\"',
|
||||
})
|
||||
status_tokens = ("blocked", "compile-only", "validated")
|
||||
status_modes = {
|
||||
name: [token for token in status_tokens if f'"{token}"' in text]
|
||||
for name, text in script_texts.items()
|
||||
}
|
||||
status_mode_present = {
|
||||
name: {
|
||||
token: f'"{token}"' in script_texts[name]
|
||||
for token in ("blocked", "compile-only")
|
||||
}
|
||||
for name in ("package-smoke.ps1", "package-smoke.sh")
|
||||
}
|
||||
readiness_alignment = count_regex(root, {
|
||||
"package-smoke.ps1": r'androidNativeValidation',
|
||||
"package-smoke.sh": r'androidNativeValidation',
|
||||
})
|
||||
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()}
|
||||
status_gate_complete = {
|
||||
"package-smoke.ps1": status_mode_present["package-smoke.ps1"]["blocked"] and status_mode_present["package-smoke.ps1"]["compile-only"],
|
||||
"package-smoke.sh": status_mode_present["package-smoke.sh"]["blocked"] and status_mode_present["package-smoke.sh"]["compile-only"],
|
||||
}
|
||||
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(status_gate_complete.values())
|
||||
and all(readiness_alignment.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,
|
||||
"statusModes": status_modes,
|
||||
"statusModePresent": status_mode_present,
|
||||
"readinessAlignment": readiness_alignment,
|
||||
"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())
|
||||
188
scripts/dev/check_renderer_api_contract.py
Normal file
188
scripts/dev/check_renderer_api_contract.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate renderer API contract purity for key rendering components."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""")
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
CHECKS = {
|
||||
"renderer_api": {
|
||||
"roots": [REPO_ROOT / "src" / "renderer_api"],
|
||||
"allowed_include_prefixes": ("foundation/", "renderer_api/"),
|
||||
"forbidden_include_tokens": (
|
||||
"renderer_gl/",
|
||||
"platform/",
|
||||
"platform_api/",
|
||||
"platform_",
|
||||
"opengl",
|
||||
"glad",
|
||||
"vulkan",
|
||||
"d3d",
|
||||
"directx",
|
||||
"metal",
|
||||
"appkit",
|
||||
"cocoa",
|
||||
"objc/",
|
||||
"windows.h",
|
||||
"x11/",
|
||||
"wayland-",
|
||||
"android/",
|
||||
),
|
||||
"forbidden_body_tokens": (
|
||||
"OpenGl",
|
||||
"OpenGL",
|
||||
"opengl_",
|
||||
"GL_",
|
||||
"Vulkan",
|
||||
"Vk",
|
||||
"MTL",
|
||||
"D3D",
|
||||
"vulkan",
|
||||
"metal",
|
||||
"renderer_gl",
|
||||
"pp_platform_",
|
||||
),
|
||||
},
|
||||
"paint_renderer": {
|
||||
"roots": [REPO_ROOT / "src" / "paint_renderer"],
|
||||
"allowed_include_prefixes": (
|
||||
"assets/",
|
||||
"document/",
|
||||
"foundation/",
|
||||
"paint/",
|
||||
"paint_renderer/",
|
||||
"renderer_api/",
|
||||
),
|
||||
"forbidden_include_tokens": (
|
||||
"renderer_gl/",
|
||||
"platform/",
|
||||
"platform_api/",
|
||||
"platform_",
|
||||
"opengl",
|
||||
"glad",
|
||||
"vulkan",
|
||||
"d3d",
|
||||
"directx",
|
||||
"metal",
|
||||
"appkit",
|
||||
"cocoa",
|
||||
"objc/",
|
||||
"windows.h",
|
||||
"x11/",
|
||||
"wayland-",
|
||||
"android/",
|
||||
),
|
||||
"forbidden_body_tokens": (
|
||||
"OpenGl",
|
||||
"OpenGL",
|
||||
"opengl_",
|
||||
"GL_",
|
||||
"Vulkan",
|
||||
"Vk",
|
||||
"MTL",
|
||||
"D3D",
|
||||
"vulkan",
|
||||
"metal",
|
||||
"renderer_gl",
|
||||
"pp_platform_",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
ALLOWED_EXTERNAL_PREFIXES = ("stb/",)
|
||||
|
||||
|
||||
def is_forbidden_include(allowlist: tuple[str, ...], forbidden_tokens: tuple[str, ...], include: str) -> tuple[bool, str | None]:
|
||||
include_lower = include.lower()
|
||||
if any(token in include_lower for token in forbidden_tokens):
|
||||
token = next(token for token in forbidden_tokens if token in include_lower)
|
||||
return True, token
|
||||
|
||||
if "/" in include and include_lower.startswith(ALLOWED_EXTERNAL_PREFIXES):
|
||||
return False, None
|
||||
|
||||
if "/" in include and not any(include_lower.startswith(prefix) for prefix in allowlist):
|
||||
return True, "cross-component-include"
|
||||
return False, None
|
||||
|
||||
|
||||
def scan_component(name: str, config: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
violations: list[dict[str, Any]] = []
|
||||
for root in config["roots"]:
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}:
|
||||
continue
|
||||
|
||||
text = path.read_text(encoding="utf-8").splitlines()
|
||||
for line_no, line in enumerate(text, start=1):
|
||||
include_match = INCLUDE_RE.match(line)
|
||||
if include_match:
|
||||
include = (include_match.group(2) or include_match.group(3) or "").strip()
|
||||
forbidden, reason = is_forbidden_include(
|
||||
config["allowed_include_prefixes"],
|
||||
config["forbidden_include_tokens"],
|
||||
include,
|
||||
)
|
||||
if forbidden:
|
||||
violations.append(
|
||||
{
|
||||
"component": name,
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": line_no,
|
||||
"kind": "forbidden-include",
|
||||
"include": include,
|
||||
"detail": reason,
|
||||
"text": line.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
joined = "\n".join(text)
|
||||
for token in config["forbidden_body_tokens"]:
|
||||
if token in joined:
|
||||
violations.append(
|
||||
{
|
||||
"component": name,
|
||||
"file": str(path.relative_to(REPO_ROOT)),
|
||||
"line": 0,
|
||||
"kind": "forbidden-body-token",
|
||||
"include": token,
|
||||
"detail": "backend- or platform-specific symbol in renderer contract path",
|
||||
"text": "",
|
||||
}
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
violations: list[dict[str, Any]] = []
|
||||
for component, config in CHECKS.items():
|
||||
violations.extend(scan_component(component, config))
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": len(violations) == 0,
|
||||
"summary": {
|
||||
"componentCount": len(CHECKS),
|
||||
"violationCount": len(violations),
|
||||
},
|
||||
"violations": violations,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
return 0 if not violations else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
119
scripts/dev/check_renderer_conformance_matrix.py
Normal file
119
scripts/dev/check_renderer_conformance_matrix.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that renderer conformance fixtures are registered and labeled consistently."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
TESTS_CMAKE = REPO_ROOT / "tests" / "CMakeLists.txt"
|
||||
|
||||
REQUIRED_TEST_LABELS = {
|
||||
"pp_renderer_api_tests": {"renderer-conformance", "renderer"},
|
||||
}
|
||||
|
||||
OPTIONAL_BACKEND_TEST_LABELS = {
|
||||
"pp_renderer_gl_capabilities_tests": {"renderer-conformance", "renderer"},
|
||||
"pp_renderer_gl_command_plan_tests": {"renderer-conformance", "renderer"},
|
||||
"pp_renderer_gl_gpu_readback_tests": {"renderer-conformance", "renderer", "gpu"},
|
||||
}
|
||||
|
||||
def parse_labels() -> dict[str, set[str]]:
|
||||
labels_by_test: dict[str, set[str]] = {}
|
||||
text = TESTS_CMAKE.read_text(encoding="utf-8").splitlines()
|
||||
i = 0
|
||||
while i < len(text):
|
||||
line = text[i].strip()
|
||||
if not line.startswith("set_tests_properties("):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if "set_tests_properties(" not in line or "PROPERTIES" not in line:
|
||||
i += 1
|
||||
continue
|
||||
after_paren = line.split("set_tests_properties(", 1)[1]
|
||||
test_name = after_paren.split()[0].strip()
|
||||
test_name = test_name.strip()
|
||||
|
||||
label_value: str | None = None
|
||||
j = i
|
||||
while j < len(text):
|
||||
search = text[j].strip()
|
||||
if search.startswith("LABELS"):
|
||||
colon = search.find("\"")
|
||||
if colon != -1:
|
||||
value = search[colon:].strip()
|
||||
if value.startswith("\"") and value.endswith("\""):
|
||||
label_value = value[1:-1]
|
||||
break
|
||||
# Fallback for multiline values: LABELS "a;b"; split on quotes in line.
|
||||
quotes = re.findall(r'"([^"]+)"', search)
|
||||
if quotes:
|
||||
label_value = quotes[0]
|
||||
break
|
||||
if search == ")" or (search.startswith(")") and "LABELS" not in search):
|
||||
break
|
||||
j += 1
|
||||
|
||||
if label_value is not None:
|
||||
labels_by_test[test_name] = {label.strip() for label in label_value.split(";") if label.strip()}
|
||||
|
||||
i = j + 1
|
||||
|
||||
return labels_by_test
|
||||
|
||||
|
||||
def validate() -> tuple[bool, list[dict[str, Any]]]:
|
||||
labels_by_test = parse_labels()
|
||||
test_names = set(labels_by_test)
|
||||
violations: list[dict[str, Any]] = []
|
||||
|
||||
for test_name, required_labels in REQUIRED_TEST_LABELS.items():
|
||||
actual = labels_by_test.get(test_name)
|
||||
if actual is None:
|
||||
violations.append({"test": test_name, "kind": "missing-test", "detail": "required conformance test not registered"})
|
||||
continue
|
||||
|
||||
missing = sorted(required_labels - actual)
|
||||
if missing:
|
||||
violations.append(
|
||||
{
|
||||
"test": test_name,
|
||||
"kind": "missing-label",
|
||||
"detail": f"required labels missing: {', '.join(missing)}",
|
||||
}
|
||||
)
|
||||
|
||||
for test_name, required_labels in OPTIONAL_BACKEND_TEST_LABELS.items():
|
||||
if test_name not in test_names:
|
||||
continue
|
||||
actual = labels_by_test[test_name]
|
||||
missing = sorted(required_labels - actual)
|
||||
if missing:
|
||||
violations.append(
|
||||
{
|
||||
"test": test_name,
|
||||
"kind": "missing-label",
|
||||
"detail": f"required labels missing: {', '.join(missing)}",
|
||||
}
|
||||
)
|
||||
|
||||
return (len(violations) == 0), violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok, violations = validate()
|
||||
payload = {
|
||||
"ok": ok,
|
||||
"summary": {
|
||||
"requiredTestCount": len(REQUIRED_TEST_LABELS),
|
||||
"violationCount": len(violations),
|
||||
},
|
||||
"violations": violations,
|
||||
}
|
||||
print(payload)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(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."
|
||||
1178
src/app.cpp
1178
src/app.cpp
File diff suppressed because it is too large
Load Diff
203
src/app.h
203
src/app.h
@@ -19,30 +19,39 @@
|
||||
#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"
|
||||
#include "app_runtime.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
|
||||
@@ -68,21 +77,6 @@ struct VRController
|
||||
virtual float get_trigger_value() const { return 1.f; }
|
||||
};
|
||||
|
||||
struct AppTask : public std::packaged_task<void()>
|
||||
{
|
||||
size_t task_id;
|
||||
#ifdef _DEBUG
|
||||
std::string name;
|
||||
#endif
|
||||
template<typename F> AppTask(F f) : std::packaged_task<void()>(f)
|
||||
{
|
||||
task_id = typeid(f).hash_code();
|
||||
#ifdef _DEBUG
|
||||
name = typeid(f).name();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
class App
|
||||
{
|
||||
public:
|
||||
@@ -91,7 +85,7 @@ public:
|
||||
std::string work_path{ "." };
|
||||
std::string rec_path{ "." };
|
||||
std::string tmp_path{ "." };
|
||||
std::thread rec_thread;
|
||||
std::jthread rec_thread;
|
||||
bool rec_running = false;
|
||||
int rec_count = 0;
|
||||
std::mutex rec_mutex;
|
||||
@@ -155,6 +149,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 +169,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 +281,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();
|
||||
@@ -270,10 +305,6 @@ public:
|
||||
void cloud_upload();
|
||||
void cloud_upload_all();
|
||||
void cloud_browse();
|
||||
void upload(std::string filename, std::string name = "",
|
||||
std::function<void(float)> progress = nullptr);
|
||||
void download(std::string url, std::string dest_filepath,
|
||||
std::function<void(float)> progress = nullptr);
|
||||
bool check_license();
|
||||
|
||||
std::shared_ptr<NodeProgressBar> show_progress(const std::string& title, int total = 0);
|
||||
@@ -300,16 +331,13 @@ public:
|
||||
|
||||
void cmd_convert(std::string pano_path, std::string out_path);
|
||||
|
||||
AppRuntime& runtime() noexcept { return runtime_; }
|
||||
const AppRuntime& runtime() const noexcept { return runtime_; }
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// RENDER THREAD
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::deque<AppTask> render_tasklist;
|
||||
static std::mutex render_task_mutex;
|
||||
static std::condition_variable render_cv;
|
||||
static std::thread render_thread;
|
||||
static std::thread::id render_thread_id;
|
||||
static bool render_running;
|
||||
void render_thread_tick();
|
||||
void render_thread_main();
|
||||
void render_thread_start();
|
||||
@@ -317,71 +345,30 @@ public:
|
||||
|
||||
bool is_render_thread()
|
||||
{
|
||||
return std::this_thread::get_id() == render_thread_id;
|
||||
return runtime_.is_render_thread();
|
||||
}
|
||||
|
||||
// don't capture a reference to this ptr as the object may be destroyed
|
||||
// by the time the task is executed
|
||||
template<typename T>
|
||||
std::future<void> render_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !render_tasklist.empty())
|
||||
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();
|
||||
}
|
||||
return f;
|
||||
return runtime_.render_task_async(std::move(task), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void render_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
render_cv.notify_all();
|
||||
}
|
||||
if (render_running)
|
||||
f.get();
|
||||
runtime_.render_task(std::move(task));
|
||||
}
|
||||
|
||||
void render_sync()
|
||||
{
|
||||
render_task([] {});
|
||||
runtime_.render_sync();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// UI THREAD
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::deque<AppTask> ui_tasklist;
|
||||
static std::mutex ui_task_mutex;
|
||||
static std::condition_variable ui_cv;
|
||||
static std::thread ui_thread;
|
||||
static std::thread::id ui_thread_id;
|
||||
static bool ui_running;
|
||||
void ui_thread_tick();
|
||||
void ui_thread_main();
|
||||
void ui_thread_start();
|
||||
@@ -389,59 +376,29 @@ public:
|
||||
|
||||
bool is_ui_thread()
|
||||
{
|
||||
return std::this_thread::get_id() == ui_thread_id;
|
||||
return runtime_.is_ui_thread();
|
||||
}
|
||||
|
||||
// don't capture a reference to this ptr as the object may be destroyed
|
||||
// by the time the task is executed
|
||||
template<typename T>
|
||||
std::future<void> ui_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !ui_tasklist.empty())
|
||||
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();
|
||||
}
|
||||
return f;
|
||||
return runtime_.ui_task_async(std::move(task), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void ui_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
if (ui_running)
|
||||
f.get();
|
||||
redraw = true;
|
||||
runtime_.ui_task(std::move(task));
|
||||
if (runtime_.request_redraw())
|
||||
redraw = true;
|
||||
runtime_.clear_request_redraw();
|
||||
}
|
||||
|
||||
void ui_sync()
|
||||
{
|
||||
ui_task([] {});
|
||||
runtime_.ui_sync();
|
||||
}
|
||||
|
||||
private:
|
||||
AppRuntime runtime_;
|
||||
};
|
||||
|
||||
@@ -1,120 +1,40 @@
|
||||
#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()
|
||||
{
|
||||
std::thread([this] {
|
||||
pp::panopainter::queue_legacy_cloud_worker_task([this] {
|
||||
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();
|
||||
|
||||
}).detach();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
207
src/app_core/app_thread.h
Normal file
207
src/app_core/app_thread.h
Normal file
@@ -0,0 +1,207 @@
|
||||
#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;
|
||||
// When true, dispatch attempts from non-target threads are rejected instead
|
||||
// of being queued for later execution.
|
||||
bool reject_unsafe_cross_thread_dispatch = 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,
|
||||
bool reject_unsafe_cross_thread_dispatch = false) noexcept
|
||||
{
|
||||
const bool queue_task = !already_on_target_thread && !reject_unsafe_cross_thread_dispatch;
|
||||
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 = !(!already_on_target_thread && reject_unsafe_cross_thread_dispatch)
|
||||
&& request_redraw_after_dispatch,
|
||||
.reject_unsafe_cross_thread_dispatch = !already_on_target_thread
|
||||
&& reject_unsafe_cross_thread_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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user