Compare commits
957 Commits
2ac2c45b11
...
codex/mode
| Author | SHA1 | Date | |
|---|---|---|---|
| 888f69b394 | |||
| e808018e53 | |||
| 90e828bca1 | |||
| c63a96cc87 | |||
| b505d9f727 | |||
| 52ed7ddeb0 | |||
| 0c609b9d15 | |||
| d632efb10f | |||
| 1a64118b2c | |||
| dd638e5af4 | |||
| 04a1c5d0b1 | |||
| bc9ba75e49 | |||
| 62984509ba | |||
| bce372f9fe | |||
| b311afedd2 | |||
| 3230da243a | |||
| 19f1af57fd | |||
| 57c2d1bfa4 | |||
| b5d3bc131d | |||
| d80289665d | |||
| ab6436a38d | |||
| 9750c418bc | |||
| 3edb6617d0 | |||
| 68917203e8 | |||
| 0249ecab28 | |||
| 624f1bb99d | |||
| a49967168a | |||
| 0065c6dd9e | |||
| 8f74c959a7 | |||
| d031e4c5fa | |||
| 1e9235cb6a | |||
| 74abddd81e | |||
| 2f33b00b2a | |||
| 065717f89b | |||
| aec78fb838 | |||
| a267386188 | |||
| e1767bdb00 | |||
| 4a5b55f58a | |||
| e343557a3f | |||
| 680452983f | |||
| 30a07888da | |||
| 18ed47aa81 | |||
| b35fa36584 | |||
| a1031b3af1 | |||
| 9602196e99 | |||
| 0cf6a6ea4f | |||
| ba94785eda | |||
| 68b8d8c45f | |||
| 8a4d611b07 | |||
| 45f3d501e7 | |||
| 949dbf778a | |||
| 81a998436d | |||
| cf2fcd36e4 | |||
| 06bfd62546 | |||
| 0a7961d8b3 | |||
| 59a9074109 | |||
| 00f97c71b5 | |||
| 3941c54d90 | |||
| f45fc8226c | |||
| 5491ed4bf5 | |||
| 4bef707c81 | |||
| af2901e78a | |||
| 2cb7046a56 | |||
| c225529cbf | |||
| f98e4f4889 | |||
| ea1845d924 | |||
| 4c91701e11 | |||
| 25eff166f6 | |||
| 10a3c0498e | |||
| 6d906f6288 | |||
| c1163e39e4 | |||
| 01ff014dd9 | |||
| d502bf9331 | |||
| e9723276be | |||
| 3930e70817 | |||
| 5c8a87faa0 | |||
| 3cbc88fe78 | |||
| fd462dc406 | |||
| 3ce365fc15 | |||
| 90a55b86fe | |||
| 5fdc9a9dd6 | |||
| 9b1e593477 | |||
| 86e57d47ad | |||
| 371095770d | |||
| acee4db356 | |||
| cb9751dcc7 | |||
| 42bae9db16 | |||
| 6b337b2d87 | |||
| dde6123598 | |||
| a8e4e02e94 | |||
| 5f76716732 | |||
| 338f115540 | |||
| 2a2f0c7dd6 | |||
| 24d9d5b6e2 | |||
| a2a67960c8 | |||
| c25af6f493 | |||
| a05afb24f3 | |||
| d6a7512b94 | |||
| 9b2a0d9c30 | |||
| f78f72b607 | |||
| 200265e11d | |||
| a5002a4e3e | |||
| b56a46a82c | |||
| 8906756d12 | |||
| 1442c13dd7 | |||
| 0441dc4077 | |||
| 69bcb1bc38 | |||
| 9c33ecc22b | |||
| 8ea56cbd30 | |||
| cb9d06c6dc | |||
| 184f662493 | |||
| acd34540e0 | |||
| 3407daff08 | |||
| 01854f9b10 | |||
| 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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,6 +39,8 @@ PanoPainterPackage/_pkginfo.txt
|
||||
PanoPainterPackage/AppPackages/
|
||||
PanoPainterPackage/BundleArtifacts/
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
steam/content/
|
||||
steam/output/
|
||||
@@ -55,3 +57,4 @@ webgl/build
|
||||
webgl/.vscode
|
||||
|
||||
out/
|
||||
Testing/
|
||||
|
||||
182
AGENTS.md
Normal file
182
AGENTS.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
`windows-msvc-default` uses the `Visual Studio 18 2026` generator. Plain
|
||||
`cmake` therefore needs a new enough global install to recognize that
|
||||
generator. When in doubt on Windows, prefer the quiet wrapper below: it
|
||||
explicitly prefers the VS-bundled CMake at
|
||||
`C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`
|
||||
and only falls back to `cmake` from `PATH` when that bundled tool is missing.
|
||||
|
||||
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.
|
||||
211
CMakeLists.txt
211
CMakeLists.txt
@@ -5,6 +5,10 @@ project(PanoPainter
|
||||
DESCRIPTION "Panoramic painting and animation application"
|
||||
LANGUAGES C CXX)
|
||||
|
||||
if(APPLE)
|
||||
enable_language(OBJCXX)
|
||||
endif()
|
||||
|
||||
if(POLICY CMP0091)
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
endif()
|
||||
@@ -21,6 +25,8 @@ 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)
|
||||
@@ -50,6 +56,13 @@ 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)
|
||||
@@ -97,6 +110,7 @@ target_link_libraries(pp_foundation
|
||||
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
|
||||
@@ -179,6 +193,17 @@ if(PP_ENABLE_OPENGL)
|
||||
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
|
||||
@@ -189,6 +214,7 @@ target_include_directories(pp_paint_renderer
|
||||
target_link_libraries(pp_paint_renderer
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
@@ -198,7 +224,9 @@ target_link_libraries(pp_paint_renderer
|
||||
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/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")
|
||||
@@ -211,6 +239,12 @@ target_link_libraries(pp_ui_core
|
||||
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_api/platform_policy.cpp
|
||||
src/platform_api/platform_policy.h
|
||||
src/platform_api/platform_services.cpp
|
||||
src/platform_api/platform_services.h)
|
||||
target_include_directories(pp_platform_api
|
||||
@@ -222,12 +256,114 @@ target_link_libraries(pp_platform_api
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_platform_apple STATIC
|
||||
${PP_PLATFORM_APPLE_SOURCES})
|
||||
if(APPLE)
|
||||
set_source_files_properties(
|
||||
src/platform_apple/apple_platform_services.cpp
|
||||
src/platform_apple/apple_platform_state.cpp
|
||||
PROPERTIES
|
||||
LANGUAGE OBJCXX)
|
||||
endif()
|
||||
target_include_directories(pp_platform_apple
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_platform_apple
|
||||
PRIVATE
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_link_libraries(pp_platform_apple
|
||||
PUBLIC
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_platform_apple PUBLIC pp_renderer_gl)
|
||||
endif()
|
||||
if(APPLE)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
target_link_libraries(pp_platform_apple
|
||||
PUBLIC
|
||||
"-framework Foundation"
|
||||
"-framework AVFoundation"
|
||||
"-framework UIKit"
|
||||
"-framework GLKit"
|
||||
"-framework OpenGLES")
|
||||
else()
|
||||
target_link_libraries(pp_platform_apple
|
||||
PUBLIC
|
||||
"-framework Foundation"
|
||||
"-framework AVFoundation"
|
||||
"-framework AppKit"
|
||||
"-framework OpenGL")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(pp_platform_linux STATIC
|
||||
${PP_PLATFORM_LINUX_SOURCES})
|
||||
target_include_directories(pp_platform_linux
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_platform_linux
|
||||
PRIVATE
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_link_libraries(pp_platform_linux
|
||||
PUBLIC
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_platform_linux PUBLIC pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
add_library(pp_platform_android STATIC
|
||||
${PP_PLATFORM_ANDROID_SOURCES})
|
||||
target_include_directories(pp_platform_android
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_platform_android
|
||||
PUBLIC
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_platform_web STATIC
|
||||
${PP_PLATFORM_WEB_SOURCES})
|
||||
target_include_directories(pp_platform_web
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_platform_web
|
||||
PRIVATE
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_link_libraries(pp_platform_web
|
||||
PUBLIC
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_platform_web PUBLIC pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
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
|
||||
@@ -252,6 +388,7 @@ target_include_directories(pp_app_core
|
||||
target_link_libraries(pp_app_core
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
@@ -267,8 +404,24 @@ 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_VENDOR_SOURCES})
|
||||
${PP_LEGACY_VENDOR_SOURCES})
|
||||
target_link_libraries(pp_legacy_vendor
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
@@ -277,19 +430,40 @@ if(PP_BUILD_APP)
|
||||
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
|
||||
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)
|
||||
${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})
|
||||
@@ -327,6 +501,7 @@ if(PP_BUILD_APP)
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_platform_api
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
@@ -356,6 +531,7 @@ if(PP_BUILD_APP)
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
@@ -384,6 +560,7 @@ if(PP_BUILD_APP)
|
||||
$<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
|
||||
@@ -429,6 +606,8 @@ if(PP_BUILD_APP)
|
||||
pp_legacy_engine
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
@@ -465,8 +644,12 @@ if(PP_BUILD_APP)
|
||||
pp_legacy_ui_core
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
target_link_libraries(pp_legacy_app
|
||||
PUBLIC
|
||||
pp_platform_linux)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
@@ -496,8 +679,11 @@ if(PP_BUILD_APP)
|
||||
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
|
||||
@@ -517,6 +703,9 @@ if(PP_BUILD_APP)
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(APPLE)
|
||||
target_link_libraries(panopainter_app PRIVATE pp_platform_apple)
|
||||
endif()
|
||||
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||
|
||||
add_library(pp_platform_windows OBJECT
|
||||
|
||||
@@ -43,12 +43,14 @@
|
||||
"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": {
|
||||
@@ -267,6 +269,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "renderer-conformance",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"filter": {
|
||||
"include": {
|
||||
"label": "renderer-conformance"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fuzz",
|
||||
"configurePreset": "windows-msvc-default",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "app.h"
|
||||
#include "keymap.h"
|
||||
#include "main.h"
|
||||
#include "platform_apple/apple_platform_services.h"
|
||||
#include "settings.h"
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Cocoa/Cocoa.h>
|
||||
@@ -22,6 +23,8 @@
|
||||
@import AppCenterAnalytics;
|
||||
@import AppCenterCrashes;
|
||||
|
||||
static std::unique_ptr<pp::platform::PlatformServices> g_platform_services;
|
||||
|
||||
NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
|
||||
{
|
||||
TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
|
||||
@@ -521,6 +524,8 @@ NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
|
||||
[MSCrashes class]
|
||||
]];
|
||||
|
||||
g_platform_services = pp::platform::apple::create_apple_platform_services();
|
||||
App::I->set_platform_services(g_platform_services.get());
|
||||
App::I->initLog();
|
||||
App::I->create();
|
||||
NSRect r = NSMakeRect(0, 0, App::I->width, App::I->height);
|
||||
@@ -533,7 +538,7 @@ NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
|
||||
|
||||
view = [[View alloc] initWithFrame:r];
|
||||
controller = [[Controller alloc] initWithWindow:window];
|
||||
App::I->osx_view = view;
|
||||
pp::platform::apple::set_legacy_apple_state(view, nullptr);
|
||||
|
||||
float z = (float)window.backingScaleFactor;
|
||||
App::I->zoom = Settings::value_or<Serializer::Float>("ui-scale", (z > 0.f) ? z : 1.f);
|
||||
@@ -581,10 +586,10 @@ NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
|
||||
{
|
||||
LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
App::I->data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
const std::string data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
|
||||
NSString* recpath = [docpath stringByAppendingString:@"/rec"];
|
||||
App::I->rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
const std::string recording_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err])
|
||||
{
|
||||
LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
@@ -612,6 +617,13 @@ NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
|
||||
{
|
||||
LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
|
||||
pp::platform::apple::set_legacy_apple_storage_paths({
|
||||
data_path,
|
||||
data_path,
|
||||
recording_path,
|
||||
{},
|
||||
});
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -625,7 +637,7 @@ int main(int argc, const char * argv[])
|
||||
return 0;
|
||||
|
||||
AppOSX* app = [AppOSX sharedApplication];
|
||||
App::I->osx_app = app;
|
||||
pp::platform::apple::set_legacy_apple_state(nullptr, app);
|
||||
[app run];
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
#import "GameViewController.h"
|
||||
#import <OpenGLES/ES3/glext.h>
|
||||
#include "app.h"
|
||||
#include "platform_apple/apple_platform_services.h"
|
||||
#include "settings.h"
|
||||
#import "objc_utils.h"
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
std::mutex render_mutex;
|
||||
std::condition_variable render_cv;
|
||||
static std::unique_ptr<pp::platform::PlatformServices> g_platform_services;
|
||||
|
||||
@interface GameImagePicker : UIImagePickerController
|
||||
{
|
||||
@@ -111,19 +113,19 @@ std::recursive_mutex lock_mutex;
|
||||
{
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString* docpath = (NSString*)[paths objectAtIndex:0];
|
||||
App::I->data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
const std::string data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
|
||||
NSError* err = nil;
|
||||
|
||||
NSString* recpath = [docpath stringByAppendingString:@"/rec"];
|
||||
App::I->rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
const std::string recording_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err])
|
||||
{
|
||||
LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
// tmp
|
||||
NSString* tmppath = [docpath stringByAppendingString:@"/tmp"];
|
||||
App::I->tmp_path = [tmppath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
const std::string temporary_path = [tmppath cStringUsingEncoding:NSASCIIStringEncoding];
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmppath withIntermediateDirectories:YES attributes:nil error:&err])
|
||||
{
|
||||
LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
@@ -151,6 +153,13 @@ std::recursive_mutex lock_mutex;
|
||||
{
|
||||
LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
|
||||
pp::platform::apple::set_legacy_apple_storage_paths({
|
||||
data_path,
|
||||
data_path,
|
||||
recording_path,
|
||||
temporary_path,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)pick_photo:(std::function<void(std::string)>) callback
|
||||
@@ -569,8 +578,11 @@ bool is_tap = true;
|
||||
[super viewDidLoad];
|
||||
input_enabled = NO;
|
||||
App::I = new App;
|
||||
App::I->ios_view = self;
|
||||
App::I->ios_app = (AppDelegate*)[[UIApplication sharedApplication] delegate];
|
||||
pp::platform::apple::set_legacy_apple_state(
|
||||
self,
|
||||
(AppDelegate*)[[UIApplication sharedApplication] delegate]);
|
||||
g_platform_services = pp::platform::apple::create_apple_platform_services();
|
||||
App::I->set_platform_services(g_platform_services.get());
|
||||
App::I->initLog();
|
||||
|
||||
//self.preferredFramesPerSecond = 60;
|
||||
|
||||
@@ -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_android/android_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_android/android_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_android/android_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
|
||||
|
||||
@@ -31,11 +31,14 @@
|
||||
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "platform_android/android_platform_services.h"
|
||||
#include "asset.h"
|
||||
#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"
|
||||
@@ -67,6 +70,8 @@ std::recursive_mutex mutex;
|
||||
int mutex_count = 0;
|
||||
struct engine g_engine;
|
||||
thread_local JNIEnv* jni;
|
||||
std::shared_ptr<pp::platform::PlatformStoragePaths> g_android_storage_paths =
|
||||
std::make_shared<pp::platform::PlatformStoragePaths>();
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/)
|
||||
{
|
||||
@@ -241,7 +246,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
|
||||
@@ -272,6 +277,12 @@ JNIEXPORT void JNICALL Java_com_omixlab_panopainter_MainActivity_pickExternalCal
|
||||
App::I->data_path = file_path;
|
||||
App::I->work_path = file_path;
|
||||
App::I->rec_path = file_path + "/frames";
|
||||
*g_android_storage_paths = {
|
||||
App::I->data_path,
|
||||
App::I->work_path,
|
||||
App::I->rec_path,
|
||||
App::I->tmp_path,
|
||||
};
|
||||
|
||||
App::I->initLog();
|
||||
}
|
||||
@@ -603,7 +614,6 @@ static int engine_init_display(struct engine* engine) {
|
||||
if (resuming_context)
|
||||
{
|
||||
LOG("RESUME APP");
|
||||
App::I->and_app = engine->app;
|
||||
LOG("release egl context");
|
||||
eglMakeCurrent(engine->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
mutex.unlock();
|
||||
@@ -700,15 +710,19 @@ 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;
|
||||
App::I->and_app = engine->app;
|
||||
App::I->and_engine = engine;
|
||||
Asset::set_android_asset_manager(engine->app->activity->assetManager);
|
||||
|
||||
//std::string base_path = engine->app->activity->externalDataPath ?
|
||||
// engine->app->activity->externalDataPath : get_data_path(engine->app);
|
||||
if (App::I->data_path.empty() || App::I->data_path == ".")
|
||||
App::I->data_path = get_data_path();
|
||||
LOG("data_path %s", App::I->data_path.c_str());
|
||||
*g_android_storage_paths = {
|
||||
App::I->data_path,
|
||||
App::I->work_path.empty() ? App::I->data_path : App::I->work_path,
|
||||
App::I->rec_path,
|
||||
App::I->tmp_path,
|
||||
};
|
||||
|
||||
|
||||
#ifdef __QUEST__
|
||||
@@ -1077,8 +1091,30 @@ void android_main(struct android_app* state) {
|
||||
// Make sure glue isn't stripped.
|
||||
// DON'T REMOVE, even if the compiler say it's deprecated
|
||||
app_dummy();
|
||||
auto platform_services = pp::platform::android::create_platform_services({
|
||||
.bridge = {
|
||||
.clipboard_text = [] { return android_get_clipboard(); },
|
||||
.set_clipboard_text = [](std::string_view text) {
|
||||
return android_set_clipboard(std::string(text));
|
||||
},
|
||||
.set_virtual_keyboard_visible = [](bool visible) { displayKeyboard(visible); },
|
||||
.attach_ui_thread = [] { android_attach_jni(); },
|
||||
.detach_ui_thread = [] { android_detach_jni(); },
|
||||
.acquire_render_context = [] { android_async_lock(); },
|
||||
.release_render_context = [] { android_async_unlock(); },
|
||||
.present_render_context = [] { android_async_swap(); },
|
||||
.pick_file = [](pp::platform::PickedPathCallback callback) {
|
||||
android_pick_file(std::move(callback));
|
||||
},
|
||||
.pick_save_file = [](pp::platform::PickedPathCallback callback) {
|
||||
android_pick_file_save(std::move(callback));
|
||||
},
|
||||
},
|
||||
.storage_paths = g_android_storage_paths,
|
||||
});
|
||||
|
||||
App::I = new App;
|
||||
App::I->set_platform_services(platform_services.get());
|
||||
|
||||
memset(&g_engine, 0, sizeof(g_engine));
|
||||
state->userData = &g_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()
|
||||
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)
|
||||
@@ -22,5 +22,8 @@ function(pp_configure_windows_runtime_payloads target_name)
|
||||
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()
|
||||
|
||||
@@ -20,8 +20,23 @@ set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
|
||||
src/bezier.cpp
|
||||
src/brush.cpp
|
||||
src/canvas.cpp
|
||||
src/legacy_canvas_render_shell_services.cpp
|
||||
src/legacy_canvas_render_shell_services.h
|
||||
src/canvas_actions.cpp
|
||||
src/canvas_layer.cpp
|
||||
src/legacy_canvas_layer_services.cpp
|
||||
src/legacy_canvas_stroke_commit_services.cpp
|
||||
src/legacy_canvas_stroke_live_services.cpp
|
||||
src/legacy_canvas_stroke_runtime_services.cpp
|
||||
src/legacy_canvas_stroke_runtime_services.h
|
||||
src/legacy_canvas_document_io_services.cpp
|
||||
src/legacy_canvas_object_draw_services.cpp
|
||||
src/legacy_canvas_object_draw_services.h
|
||||
src/legacy_canvas_camera_services.cpp
|
||||
src/legacy_canvas_projection_services.cpp
|
||||
src/legacy_canvas_projection_services.h
|
||||
src/legacy_canvas_plane_data.cpp
|
||||
src/legacy_canvas_state_services.cpp
|
||||
src/event.cpp
|
||||
)
|
||||
|
||||
@@ -34,6 +49,18 @@ set(PP_LEGACY_RENDERER_GL_SOURCES
|
||||
)
|
||||
|
||||
set(PP_LEGACY_UI_CORE_SOURCES
|
||||
src/legacy_ui_node_attributes.cpp
|
||||
src/legacy_ui_node_attributes.h
|
||||
src/legacy_ui_node_loader.cpp
|
||||
src/legacy_ui_node_loader.h
|
||||
src/legacy_ui_node_event.cpp
|
||||
src/legacy_ui_node_event.h
|
||||
src/legacy_ui_node_lifecycle.cpp
|
||||
src/legacy_ui_node_lifecycle.h
|
||||
src/legacy_ui_node_style.cpp
|
||||
src/legacy_ui_node_style.h
|
||||
src/legacy_ui_node_execution.cpp
|
||||
src/legacy_ui_node_tree_services.cpp
|
||||
src/layout.cpp
|
||||
src/node.cpp
|
||||
src/node_border.cpp
|
||||
@@ -58,24 +85,151 @@ set(PP_LEGACY_UI_CORE_SOURCES
|
||||
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
src/canvas_modes.cpp
|
||||
src/legacy_canvas_mode_fill.cpp
|
||||
src/legacy_canvas_mode_pen_line.cpp
|
||||
src/legacy_canvas_mode_helpers.cpp
|
||||
src/legacy_document_image_import_services.cpp
|
||||
src/legacy_canvas_mode_helpers.h
|
||||
src/legacy_canvas_mode_mask.cpp
|
||||
src/legacy_canvas_mode_transform.cpp
|
||||
src/legacy_app_shell_services.cpp
|
||||
src/legacy_app_shell_services.h
|
||||
src/legacy_app_status_services.cpp
|
||||
src/legacy_app_status_services.h
|
||||
src/legacy_app_ui_state_services.cpp
|
||||
src/legacy_app_ui_state_services.h
|
||||
src/legacy_about_menu_binding_services.cpp
|
||||
src/legacy_about_menu_binding_services.h
|
||||
src/legacy_app_shell_performance_test_services.cpp
|
||||
src/legacy_app_shell_performance_test_services.h
|
||||
src/legacy_info_dialog_services.cpp
|
||||
src/legacy_info_dialog_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_image_import_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_PLATFORM_ANDROID_SOURCES
|
||||
src/platform_android/android_platform_services.cpp
|
||||
src/platform_android/android_platform_services.h
|
||||
)
|
||||
|
||||
set(PP_PLATFORM_WEB_SOURCES
|
||||
src/platform_web/web_platform_services.cpp
|
||||
src/platform_web/web_platform_services.h
|
||||
)
|
||||
|
||||
set(PP_PLATFORM_APPLE_SOURCES
|
||||
src/platform_apple/apple_platform_state.cpp
|
||||
src/platform_apple/apple_platform_services.cpp
|
||||
src/platform_apple/apple_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_workflow.cpp
|
||||
src/app_dialogs_export.cpp
|
||||
src/app_dialogs_info_openers.cpp
|
||||
src/app_events.cpp
|
||||
src/app_layout.cpp
|
||||
src/app_layout_bootstrap.cpp
|
||||
src/app_layout_brush.cpp
|
||||
src/app_layout_draw_toolbar.cpp
|
||||
src/legacy_draw_toolbar_binding_services.cpp
|
||||
src/legacy_draw_toolbar_binding_services.h
|
||||
src/app_layout_ui_state.cpp
|
||||
src/app_layout_sidebar.cpp
|
||||
src/legacy_sidebar_grid_popup_services.cpp
|
||||
src/legacy_sidebar_grid_popup_services.h
|
||||
src/legacy_sidebar_stroke_popup_services.cpp
|
||||
src/legacy_sidebar_stroke_popup_services.h
|
||||
src/legacy_sidebar_color_popup_services.cpp
|
||||
src/legacy_sidebar_color_popup_services.h
|
||||
src/app_layout_main_toolbar.cpp
|
||||
src/app_layout_edit_menu.cpp
|
||||
src/app_layout_about_layer_menu.cpp
|
||||
src/app_layout_file_menu.cpp
|
||||
src/legacy_file_menu_binding_services.cpp
|
||||
src/legacy_file_menu_binding_services.h
|
||||
src/app_layout_tools_menu.cpp
|
||||
src/legacy_tools_menu_binding_services.cpp
|
||||
src/legacy_tools_menu_binding_services.h
|
||||
src/app_shaders.cpp
|
||||
src/app_vr.cpp
|
||||
src/platform_legacy/legacy_platform_services.cpp
|
||||
src/platform_legacy/legacy_platform_services.h
|
||||
src/legacy_main_toolbar_binding_services.cpp
|
||||
src/legacy_main_toolbar_binding_services.h
|
||||
src/legacy_app_runtime_shell_services.cpp
|
||||
src/legacy_app_frame_services.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_brush_panel_item_ui.cpp
|
||||
src/legacy_brush_panel_item_ui.h
|
||||
src/legacy_brush_preset_services.cpp
|
||||
src/legacy_brush_preset_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/version.cpp
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_UI_SOURCES
|
||||
src/legacy_brush_ui_services.cpp
|
||||
src/legacy_brush_ui_services.h
|
||||
src/legacy_brush_panel_services.cpp
|
||||
src/legacy_brush_panel_services.h
|
||||
src/legacy_brush_panel_ui.cpp
|
||||
src/legacy_brush_panel_ui.h
|
||||
src/legacy_brush_preset_list_services.cpp
|
||||
src/legacy_brush_preset_list_services.h
|
||||
src/legacy_document_animation_services.cpp
|
||||
src/legacy_document_animation_services.h
|
||||
src/legacy_node_canvas_draw_services.cpp
|
||||
src/legacy_node_canvas_draw_services.h
|
||||
src/legacy_node_canvas_state_services.cpp
|
||||
src/legacy_node_canvas_state_services.h
|
||||
src/legacy_brush_preset_panel_ui.cpp
|
||||
src/legacy_brush_preset_panel_ui.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
|
||||
@@ -98,6 +252,12 @@ set(PP_PANOPAINTER_UI_SOURCES
|
||||
src/node_panel_layer.cpp
|
||||
src/node_panel_quick.cpp
|
||||
src/node_panel_stroke.cpp
|
||||
src/legacy_node_stroke_preview_draw_services.cpp
|
||||
src/legacy_node_stroke_preview_draw_services.h
|
||||
src/legacy_node_stroke_preview_runtime_services.cpp
|
||||
src/legacy_node_stroke_preview_runtime_services.h
|
||||
src/legacy_node_stroke_preview_sample_services.cpp
|
||||
src/legacy_node_stroke_preview_sample_services.h
|
||||
src/node_stroke_preview.cpp
|
||||
src/node_tool_bucket.cpp
|
||||
src/node_usermanual.cpp
|
||||
@@ -106,8 +266,31 @@ set(PP_PANOPAINTER_UI_SOURCES
|
||||
|
||||
set(PP_WINDOWS_PLATFORM_SOURCES
|
||||
src/main.cpp
|
||||
src/platform_windows/windows_bootstrap_helpers.cpp
|
||||
src/platform_windows/windows_async_render_context.cpp
|
||||
src/platform_windows/windows_async_render_context.h
|
||||
src/platform_windows/windows_lifecycle_shell.cpp
|
||||
src/platform_windows/windows_lifecycle_shell.h
|
||||
src/platform_windows/windows_lifecycle_state.cpp
|
||||
src/platform_windows/windows_lifecycle_state.h
|
||||
src/platform_windows/windows_main_window_session.cpp
|
||||
src/platform_windows/windows_main_window_session.h
|
||||
src/platform_windows/windows_platform_services.cpp
|
||||
src/platform_windows/windows_platform_services.h
|
||||
src/platform_windows/windows_runtime_flow.cpp
|
||||
src/platform_windows/windows_runtime_flow.h
|
||||
src/platform_windows/windows_runtime_shell.cpp
|
||||
src/platform_windows/windows_runtime_shell.h
|
||||
src/platform_windows/windows_runtime_session.cpp
|
||||
src/platform_windows/windows_runtime_session.h
|
||||
src/platform_windows/windows_runtime_state.cpp
|
||||
src/platform_windows/windows_runtime_state.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
|
||||
src/platform_windows/windows_window_shell.cpp
|
||||
src/platform_windows/windows_window_shell.h
|
||||
)
|
||||
|
||||
set(PP_WINDOWS_APP_SOURCES
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-03
|
||||
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,
|
||||
@@ -11,22 +11,23 @@ and validation command.
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests |
|
||||
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
|
||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
||||
| 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
|
||||
|
||||
@@ -37,15 +38,15 @@ and validation command.
|
||||
| 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, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke-feedback destination-copy coverage, and GPU parity |
|
||||
| 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 add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document` | Undo/redo invariant tests |
|
||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
|
||||
| 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 |
|
||||
@@ -56,8 +57,9 @@ and validation command.
|
||||
| --- | --- | --- | --- |
|
||||
| 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 and layout tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts |
|
||||
| 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 |
|
||||
|
||||
@@ -66,10 +68,11 @@ and validation command.
|
||||
| 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 |
|
||||
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
|
||||
| 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 |
|
||||
|
||||
|
||||
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.
|
||||
File diff suppressed because it is too large
Load Diff
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
804
docs/modernization/tasks.md
Normal file
804
docs/modernization/tasks.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# Modernization Task Tracker
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-17
|
||||
|
||||
This file is the active execution queue. It is written for a coordinator that
|
||||
can assign bounded packets to smaller parallel workers. Completed and stale
|
||||
history belongs in `docs/modernization/tasks-done.md`, not here.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Prioritize working-app ownership transfer over planners, CLI commands,
|
||||
package-only cleanup, or test-only work.
|
||||
- Every coding task must remove or narrow a real retained dependency, hotspot,
|
||||
unsafe ownership path, or thread/runtime ambiguity.
|
||||
- Tests are validation and guardrails. A task that only adds tests is not a P0
|
||||
modernization slice unless it directly enables a blocked ownership move.
|
||||
- Do not broaden worker scopes. If a task crosses file boundaries, split it
|
||||
into worker packets with disjoint write scopes and integrate centrally.
|
||||
- No new `App::I`, `Canvas::I`, owning raw `Node*`, detached worker, direct GL
|
||||
resource dependency, or platform SDK dependency may be introduced in moved
|
||||
code.
|
||||
- Raw pointers may remain only as documented non-owning implementation details
|
||||
backed by a checked owner, handle, scoped connection, or explicit lifetime
|
||||
contract.
|
||||
- Preserve current app behavior first. UI appearance, file formats, brush
|
||||
behavior, platform behavior, and rendering output are not to be redesigned in
|
||||
modernization slices.
|
||||
- Use CMake source ownership as the progress signal. Shrinking
|
||||
`PP_PANOPAINTER_*` and `PP_LEGACY_*` ownership matters more than adding new
|
||||
helpers around the same retained code.
|
||||
|
||||
## Current Audit Snapshot
|
||||
|
||||
Validation performed during the 2026-06-17 review:
|
||||
|
||||
- `python scripts/dev/check_component_boundaries.py`: passed.
|
||||
- `python scripts/dev/check_renderer_api_contract.py`: passed.
|
||||
|
||||
Key facts:
|
||||
|
||||
- Pure component boundaries currently pass their static checks.
|
||||
- Remaining architectural risk is concentrated in the working app, retained
|
||||
app/UI/canvas targets, singleton reach, raw node ownership, and direct GL
|
||||
resource usage.
|
||||
- `PP_PANOPAINTER_APP_SOURCES`: 47 files, about 9620 lines.
|
||||
- `PP_PANOPAINTER_UI_SOURCES`: 52 files, about 9051 lines.
|
||||
- `PP_LEGACY_PAINT_DOCUMENT_SOURCES`: 22 files, about 6277 lines.
|
||||
- `PP_LEGACY_APP_SOURCES`: 26 files, about 4711 lines.
|
||||
- `PP_LEGACY_UI_CORE_SOURCES`: 32 files, about 4304 lines.
|
||||
- `App::I` still appears hundreds of times in retained app/canvas/UI/resource
|
||||
code.
|
||||
- `Canvas::I` still appears hundreds of times in retained canvas modes, panels,
|
||||
and workflow bridges.
|
||||
- Raw `Node*` and callback captures remain a dominant UI lifetime risk.
|
||||
- Retained stroke-preview/runtime draw paths still depend on legacy
|
||||
render/runtime helpers, but `RTT`, `Texture2D`, `Shape`, `Shader`,
|
||||
`TextMesh`, and `CanvasLayer` no longer call `App::I` directly for queueing.
|
||||
- `AppRuntime` now owns synchronized running flags plus explicit post/reject,
|
||||
same-thread execution, and queue-drain behavior, but broader singleton reach
|
||||
and app-shell ownership remain.
|
||||
- Retained cloud upload/download, brush-package import, and timelapse-export
|
||||
async paths now route through `AppRuntime::canvas_async_task`, but dialog and
|
||||
execution ownership still remains in retained app/document/cloud bridges.
|
||||
- `App::dialog_browse()` no longer owns browse-dialog button wiring inline; the
|
||||
retained document-open bridge now owns that handoff in
|
||||
`src/legacy_document_open_services.*`.
|
||||
- `App::dialog_open()`, `App::dialog_browse()`, and `App::dialog_resize()` now
|
||||
delegate retained dialog construction and overlay/button wiring through
|
||||
`src/legacy_document_open_services.*` and
|
||||
`src/legacy_document_session_services.*`, so
|
||||
`src/app_dialogs_workflow.cpp` is thinner while the save-before-workflow
|
||||
policy seam remains local.
|
||||
- `App::init_toolbar_main()` now delegates retained main-toolbar button wiring
|
||||
through `src/legacy_main_toolbar_binding_services.*`, so
|
||||
`src/app_layout_main_toolbar.cpp` is down to a thin root lookup and adapter
|
||||
call while retained toolbar execution still lives in
|
||||
`src/legacy_app_shell_services.*`.
|
||||
- `App::init_menu_file()` now delegates retained File-menu popup and export
|
||||
submenu wiring through `src/legacy_file_menu_binding_services.*`, so
|
||||
`src/app_layout_file_menu.cpp` is down to a thin trigger lookup and adapter
|
||||
call while retained file/export execution still lives in
|
||||
`src/legacy_app_shell_services.*`.
|
||||
- `App::init_menu_about()` now delegates retained About-menu popup wiring
|
||||
through `src/legacy_about_menu_binding_services.*`, so
|
||||
`src/app_layout_about_layer_menu.cpp` no longer owns the About callback body
|
||||
inline while retained About execution still lives in
|
||||
`src/legacy_app_shell_services.*`.
|
||||
- `App::init_menu_tools()` now delegates the retained Tools > Panels submenu
|
||||
wiring through `src/legacy_tools_menu_binding_services.*`, so
|
||||
`src/app_layout_tools_menu.cpp` no longer owns that floating-panel submenu
|
||||
body inline while retained Tools execution and options wiring remain.
|
||||
- `App::update()` now delegates retained app-frame layout update and
|
||||
canvas-toolbar refresh execution through `src/legacy_app_frame_services.*`,
|
||||
so `src/legacy_app_runtime_shell_services.cpp` is thinner at the frame
|
||||
update seam while draw-time execution still remains there.
|
||||
- `App::tick()` and `App::resize()` now delegate retained app-frame tick and
|
||||
surface-resize execution through `src/legacy_app_frame_services.*`, so
|
||||
`src/app_events.cpp` is thinner at the frame-execution seam while broader
|
||||
input/platform dispatch still remains there.
|
||||
- `App::ui_save()` and `App::ui_restore()` now delegate retained floating and
|
||||
docked panel persistence through `src/legacy_app_ui_state_services.*`, so
|
||||
`src/app_layout_ui_state.cpp` is down to thin preference adapters while RTL
|
||||
direction execution stays local.
|
||||
- `App::init_sidebar()` now delegates the retained color-popup open/close
|
||||
wiring through `src/legacy_sidebar_color_popup_services.*` with explicit
|
||||
`App&`, popup-root, trigger-button, and panel dependencies, so
|
||||
`src/app_layout_sidebar.cpp` is thinner while the retained stroke/grid/layer
|
||||
popup families still remain inline.
|
||||
- `App::dialog_usermanual()`, `App::dialog_changelog()`, and
|
||||
`App::dialog_about()` now delegate the retained info-dialog construction and
|
||||
overlay close wiring through `src/legacy_info_dialog_services.*` with
|
||||
explicit `App&` plus overlay-anchor dependencies, and the remaining
|
||||
What's New plus shortcuts flows now route through the same seam, so
|
||||
`src/app_dialogs_info_openers.cpp` is down to thin forwarding only.
|
||||
- `App::dialog_newdoc()` and `App::dialog_save()` now delegate retained dialog
|
||||
construction and button wiring through
|
||||
`src/legacy_document_session_services.*`, so
|
||||
`src/app_dialogs_workflow.cpp` is thinner at the document-session seam.
|
||||
- `App::title_update()` now delegates retained document-title and DPI-label
|
||||
rendering through `src/legacy_app_status_services.*`, so
|
||||
`src/app_layout.cpp` no longer owns that app-status family inline.
|
||||
- `App::init_toolbar_draw()` now delegates retained draw-toolbar button lookup,
|
||||
click wiring, and default-tool application through
|
||||
`src/legacy_draw_toolbar_binding_services.*`, so
|
||||
`src/app_layout_draw_toolbar.cpp` is down to a thin adapter while retained
|
||||
tool execution still flows through `src/legacy_canvas_tool_services.*`.
|
||||
- `App::init_sidebar()` now delegates the retained stroke-popup open/anchor/tick
|
||||
wiring through `src/legacy_sidebar_stroke_popup_services.*` with explicit
|
||||
`App&`, popup-root, trigger-button, and panel dependencies, so
|
||||
`src/app_layout_sidebar.cpp` is thinner while the retained layer popup family
|
||||
still remains inline.
|
||||
- `App::init_sidebar()` now delegates the retained grid-popup
|
||||
open/anchor/tick/close wiring through
|
||||
`src/legacy_sidebar_grid_popup_services.*`, so the `btn-grids-panel` path in
|
||||
`src/app_layout_sidebar.cpp` is down to a thin adapter.
|
||||
- `src/app_dialogs_export.cpp` is now a forwarding adapter; the retained
|
||||
document export start/branching flows live in
|
||||
`src/legacy_document_export_services.*`, and the PPBR dialog opener now lives
|
||||
in `src/legacy_brush_package_export_services.*`.
|
||||
- The startup/runtime stability slice narrowed several live risks at once: the
|
||||
legacy UI loader again routes XML attributes through virtual node parsers,
|
||||
`NodeComboBox` now guards empty and out-of-range item state, the extracted
|
||||
File-menu binding no longer leaves click callbacks pointing at a dead stack
|
||||
service object, and the cloud-browse dialog now queues file-list/thumbnail UI
|
||||
updates onto the UI thread instead of mutating nodes directly from its worker.
|
||||
|
||||
## Parallel Assignment Rules
|
||||
|
||||
Coordinator packets for workers should include only:
|
||||
|
||||
- task id and one-paragraph goal
|
||||
- exact write scope
|
||||
- allowed read scope
|
||||
- debt ids that matter
|
||||
- required validation command
|
||||
- specific `rg` or `clangd_nav.py` queries
|
||||
- current behavior notes needed to avoid broad rediscovery
|
||||
|
||||
Safe parallel groups:
|
||||
|
||||
- One worker on canvas/render execution, one worker on generic UI controls, one
|
||||
worker on platform CMake cleanup, and one worker on runtime queue contracts
|
||||
can run in parallel if write scopes remain disjoint.
|
||||
- Do not run two workers against `src/app_runtime.*`, `src/node.*`,
|
||||
`src/legacy_canvas_document_io_services.cpp`, or
|
||||
`src/legacy_node_stroke_preview_runtime_services.cpp` at the same time.
|
||||
- Do not assign both a CMake source-list move and code edits touching the same
|
||||
source files to separate workers unless the coordinator serializes the CMake
|
||||
integration.
|
||||
|
||||
## P0 Queue
|
||||
|
||||
### ARC-RUN-010 - Harden `AppRuntime` Into An Explicit Runtime Service
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Render/UI/background queues are central to memory and thread safety. The
|
||||
current `AppRuntime` owns several `std::jthread` workers, but runtime state is
|
||||
still mutable, partly unsynchronized, and app-specific. The working app still
|
||||
uses `App::I` as the practical access path to render/UI queues.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/app_runtime.h`
|
||||
- `src/app_runtime.cpp`
|
||||
- `src/app.h`
|
||||
- `src/legacy_app_runtime_shell_services.cpp`
|
||||
- `src/app_core/app_thread.h`
|
||||
- `tests/app_core/app_thread_tests.cpp`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/texture.cpp`
|
||||
- `src/rtt.cpp`
|
||||
- `src/shape.cpp`
|
||||
- `src/shader.cpp`
|
||||
- `src/platform_windows/windows_platform_services.cpp`
|
||||
|
||||
Required work:
|
||||
|
||||
- Make render/UI/prepared-file/canvas worker running state synchronized or
|
||||
atomic, with a single shutdown path per worker.
|
||||
- Add explicit runtime service methods for thread-affinity checks and
|
||||
post/drain/shutdown semantics.
|
||||
- Keep exceptions from escaping worker bodies.
|
||||
- Stop exposing queue usage only through `App::I` wrappers for touched call
|
||||
sites.
|
||||
- Keep behavior identical for same-thread immediate execution and blocking
|
||||
render/UI calls.
|
||||
|
||||
Done when:
|
||||
|
||||
- Touched queue state has no unsynchronized read/write ambiguity.
|
||||
- Worker shutdown drains or rejects queued work according to documented
|
||||
behavior.
|
||||
- Touched app code can call an explicit runtime service instead of reaching
|
||||
queues through singleton state.
|
||||
- App-thread planner tests cover shutdown, stopped-worker enqueue, same-thread
|
||||
execution, and queue-drain behavior that the live runtime implements.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_app_core_app_thread_tests -TestRegex "pp_app_core_app_thread"
|
||||
python scripts/dev/check_component_boundaries.py
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start by auditing `render_running_`, `ui_running_`,
|
||||
`prepared_file_running_`, `canvas_async_running_`, thread ids, and worker stop
|
||||
methods. Do not rewrite GL resources in this task; expose the runtime contract
|
||||
needed for the next task.
|
||||
|
||||
### ARC-RND-010 - Move GL Resource Queueing Behind Renderer Runtime Contracts
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still use
|
||||
`App::I->render_task*` directly. That blocks renderer backends and hides thread
|
||||
affinity behind a global app singleton.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/texture.cpp`
|
||||
- `src/texture.h`
|
||||
- `src/rtt.cpp`
|
||||
- `src/rtt.h`
|
||||
- `src/shape.cpp`
|
||||
- `src/shape.h`
|
||||
- `src/shader.cpp`
|
||||
- `src/shader.h`
|
||||
- `src/font.cpp`
|
||||
- `src/font.h`
|
||||
- `src/canvas_layer.cpp`
|
||||
- `src/canvas_layer.h`
|
||||
- narrow adapter files if introduced under `src/renderer_gl/`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/app_runtime.*`
|
||||
- `src/renderer_api/*`
|
||||
- `src/renderer_gl/*`
|
||||
- `src/paint_renderer/*`
|
||||
|
||||
Required work:
|
||||
|
||||
- Introduce a narrow render-dispatch interface or adapter consumed by retained
|
||||
GL resource classes.
|
||||
- Convert one coherent GL resource family per slice; do not edit every file in
|
||||
one worker pass unless the abstraction is already integrated.
|
||||
- Preserve blocking versus async semantics exactly.
|
||||
- Do not move app policy into `pp_renderer_gl`.
|
||||
- Do not add future backend implementation work.
|
||||
|
||||
Done when:
|
||||
|
||||
- The touched GL resource family no longer calls `App::I` directly for queueing
|
||||
or thread checks.
|
||||
- Render-thread assertions use an explicit runtime/render-dispatch contract.
|
||||
- CMake ownership remains consistent and no pure renderer API target depends on
|
||||
app headers.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_renderer_api_tests,pp_renderer_gl_capabilities_tests -TestRegex "pp_renderer|pp_paint_renderer"
|
||||
python scripts/dev/check_renderer_api_contract.py
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start with either `Texture2D`/`RTT` or `Shape`/`Shader`, not both. Use `rg -n
|
||||
"App::I->render_task|is_render_thread" src/texture.* src/rtt.*` for the first
|
||||
slice.
|
||||
|
||||
### ARC-RND-011 - Split Canvas Document I/O From Render Execution
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`src/legacy_canvas_document_io_services.cpp` is still the largest working-app
|
||||
document/export hotspot and has the highest `App::I` concentration found in the
|
||||
review. It mixes license checks, worker dispatch, render readback, progress UI,
|
||||
platform publish/flush, and retained `Canvas` mutation.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/legacy_canvas_document_io_services.cpp`
|
||||
- `src/legacy_canvas_document_io_services.h`
|
||||
- `src/legacy_document_export_services.*`
|
||||
- `src/app_core/document_export.h`
|
||||
- `src/paint_renderer/*`
|
||||
- focused tests under `tests/app_core` or `tests/paint_renderer`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/canvas.*`
|
||||
- `src/canvas_layer.*`
|
||||
- `src/legacy_canvas_render_shell_services.*`
|
||||
- `src/platform_api/platform_services.h`
|
||||
|
||||
Required work:
|
||||
|
||||
- Pick one export/import family first: equirectangular export, cube-face export,
|
||||
layer/frame collection export, or project save/open async I/O.
|
||||
- Move orchestration into an app-core or paint-renderer service request that
|
||||
accepts explicit document/render/platform dependencies.
|
||||
- Leave retained `Canvas` as a final adapter only for data that has not moved.
|
||||
- Remove direct `App::I` calls from the touched path.
|
||||
- Preserve progress and platform publish behavior.
|
||||
|
||||
Done when:
|
||||
|
||||
- One live document/export path is executable through an explicit service
|
||||
request rather than by walking `App::I`/`Canvas::I` from the bridge.
|
||||
- The retained bridge is visibly thinner and has fewer reasons to know about
|
||||
UI, worker, platform, and renderer details at the same time.
|
||||
- The touched path has focused validation that exercises the new request
|
||||
contract and the retained adapter.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_export_tests,pp_paint_renderer_compositor_tests -TestRegex "document_export|paint_renderer"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Do not broaden into all export types. Start with the path that already has the
|
||||
strongest pure planning/readiness coverage, then remove only the corresponding
|
||||
direct app/canvas singleton reach.
|
||||
|
||||
### ARC-RND-012 - Make Stroke Preview A Renderer-Owned Service
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`NodeStrokePreview` has been thinned, but
|
||||
`src/legacy_node_stroke_preview_runtime_services.cpp` still owns static worker
|
||||
state, render-context handoff, preview texture lifetime, and direct app/canvas
|
||||
access. This is a high-risk UI/render/thread boundary.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/node_stroke_preview.*`
|
||||
- `src/legacy_node_stroke_preview_runtime_services.*`
|
||||
- `src/legacy_node_stroke_preview_draw_services.*`
|
||||
- `src/legacy_node_stroke_preview_sample_services.*`
|
||||
- `src/paint_renderer/*`
|
||||
- `tests/paint_renderer/*`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/app_runtime.*`
|
||||
- `src/texture.*`
|
||||
- `src/rtt.*`
|
||||
- `src/canvas.*`
|
||||
- `src/node_panel_stroke.*`
|
||||
|
||||
Required work:
|
||||
|
||||
- Move one preview execution phase behind a renderer-facing service contract.
|
||||
- Replace static worker/resource state for the touched phase with owned service
|
||||
state or explicit runtime dependency.
|
||||
- Remove direct `App::I`/`Canvas::I` from the touched phase.
|
||||
- Preserve preview output and cancellation/shutdown behavior.
|
||||
|
||||
Done when:
|
||||
|
||||
- The touched preview phase can be reasoned about without reading the full node
|
||||
implementation.
|
||||
- Preview worker lifetime is owned and cancellable for the touched phase.
|
||||
- Renderer-facing tests cover the contract without linking app texture objects.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_paint_renderer_compositor_tests -TestRegex "paint_renderer|stroke_preview"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start with result copy, live pass request assembly, or worker lifecycle. Do not
|
||||
combine all preview phases in one slice.
|
||||
|
||||
### ARC-UI-010 - Move Generic Controls Out Of `pp_legacy_ui_core`
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Generic controls still live in `PP_LEGACY_UI_CORE_SOURCES`, keeping
|
||||
`pp_panopainter_ui` tied to retained app/UI targets. This is working-app UI
|
||||
architecture, not cosmetic cleanup.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/node_button.*`
|
||||
- `src/node_checkbox.*`
|
||||
- `src/node_icon.*`
|
||||
- `src/node_image.*`
|
||||
- `src/node_scroll.*`
|
||||
- `src/node_slider.*`
|
||||
- `src/node_text.*`
|
||||
- `src/node_text_input.*`
|
||||
- `src/ui_core/*`
|
||||
- `cmake/PanoPainterSources.cmake`
|
||||
- `CMakeLists.txt`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/node.*`
|
||||
- `src/layout.*`
|
||||
- app-specific `src/node_panel_*`
|
||||
- app-specific `src/node_dialog_*`
|
||||
|
||||
Required work:
|
||||
|
||||
- Move one generic control family at a time to `pp_ui_core`.
|
||||
- Split renderer-neutral state/event/layout logic from retained GL drawing
|
||||
when a control still depends on GL classes.
|
||||
- Keep app-specific panels and dialogs out of `pp_ui_core`.
|
||||
- Update CMake ownership so the source-list change is real.
|
||||
|
||||
Done when:
|
||||
|
||||
- At least one generic control family is owned by `pp_ui_core` or has its
|
||||
renderer-neutral core owned there with only a narrow retained draw adapter.
|
||||
- `PP_LEGACY_UI_CORE_SOURCES` shrinks.
|
||||
- Existing UI behavior is unchanged.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_layout_xml_tests,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "pp_ui_core"
|
||||
python scripts/dev/check_component_boundaries.py
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start with the least app-specific control: checkbox, button, icon, image,
|
||||
scroll, slider, text, or text input. Do not touch panels/dialogs in the same
|
||||
slice.
|
||||
|
||||
### ARC-UI-011 - Convert UI Ownership To Checked Handles By Default
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`pp_ui_core` has checked lifetime helpers, but base `Node` and app panels still
|
||||
mix raw parent/manager pointers, shared child vectors, raw callback parameters,
|
||||
and destroy-during-callback assumptions.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/node.*`
|
||||
- `src/layout.*`
|
||||
- `src/legacy_ui_overlay_services.*`
|
||||
- one dialog or panel family per slice under `src/node_dialog_*` or
|
||||
`src/node_panel_*`
|
||||
- `src/ui_core/node_lifetime.*`
|
||||
- `src/ui_core/overlay_lifetime.*`
|
||||
|
||||
Read scope:
|
||||
|
||||
- call sites found with `rg -n "add_child|remove_child|destroy\\(|on_.*=|Node\\*" src/node_dialog_* src/node_panel_* src/legacy_ui_* src/node.*`
|
||||
|
||||
Required work:
|
||||
|
||||
- Convert one popup/dialog/panel family to checked handles or scoped
|
||||
connections.
|
||||
- Remove raw lifetime assumptions from callbacks in the touched family.
|
||||
- Document any remaining raw `Node*` as non-owning views with owner proof.
|
||||
- Keep visual behavior and event ordering unchanged.
|
||||
|
||||
Done when:
|
||||
|
||||
- The touched UI family can close during callback dispatch without relying on
|
||||
dangling raw pointers.
|
||||
- Overlay/popup lifetime flows through `pp_ui_core` lifetime primitives by
|
||||
default.
|
||||
- New touched callbacks are scoped or handle-checked.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "ui_core_(node_lifetime|overlay_lifetime)"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Pick one family only, such as open/browse dialogs, picker dialog, popup menu,
|
||||
layer panel, or stroke panel. Avoid broad `Node` redesign unless the family
|
||||
requires a small base helper.
|
||||
|
||||
### ARC-APP-010 - Reduce App Shells To Composition And Adapters
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
`PP_PANOPAINTER_APP_SOURCES` is still about 9620 lines. The app shell owns
|
||||
workflow, dialogs, layout binding, runtime, VR, cloud, brush package, platform
|
||||
hooks, and retained document/export adapters.
|
||||
|
||||
Write scope:
|
||||
|
||||
- one `src/app_*.cpp` family per slice
|
||||
- matching `src/legacy_app_*` service files
|
||||
- matching app-core planner/service headers only when needed
|
||||
- `cmake/PanoPainterSources.cmake` if ownership moves
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/app.h`
|
||||
- relevant app-core headers under `src/app_core`
|
||||
- relevant UI node files for the touched workflow
|
||||
|
||||
Required work:
|
||||
|
||||
- Pick one shell family: layout menus, dialogs, startup/frame, cloud, brush
|
||||
package, recording, VR, or document session.
|
||||
- Move retained implementation into a named service with explicit dependencies.
|
||||
- Make the app method a thin adapter or composition call.
|
||||
- Do not add planner-only coverage unless the app method actually shrinks.
|
||||
|
||||
Done when:
|
||||
|
||||
- One app shell file loses real workflow/runtime ownership.
|
||||
- The new service accepts explicit `App&`, runtime, platform, UI, document, or
|
||||
renderer dependencies instead of reading global state internally.
|
||||
- The touched path has focused validation or an app build gate.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start with a single app shell family. Do not mix dialogs, layout, cloud, and VR
|
||||
in one worker assignment.
|
||||
|
||||
### ARC-PLT-010 - Finish Platform Source Ownership In CMake
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Platform implementation ownership improved, but CMake still leaks Web platform
|
||||
sources into `PP_PANOPAINTER_APP_SOURCES`. Platform implementation files should
|
||||
belong to concrete `pp_platform_*` targets, not the app source group.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `cmake/PanoPainterSources.cmake`
|
||||
- `CMakeLists.txt`
|
||||
- `src/platform_web/*` only if build integration requires a narrow include or
|
||||
factory adjustment
|
||||
|
||||
Read scope:
|
||||
|
||||
- `webgl/CMakeLists.txt`
|
||||
- `src/platform_android/*`
|
||||
- `src/platform_linux/*`
|
||||
- `src/platform_apple/*`
|
||||
- `src/platform_api/platform_services.h`
|
||||
|
||||
Required work:
|
||||
|
||||
- Remove `${PP_PLATFORM_WEB_SOURCES}` from `PP_PANOPAINTER_APP_SOURCES`.
|
||||
- Ensure root app/platform targets link `pp_platform_web` only when needed.
|
||||
- Keep WebGL retained package entrypoint behavior unchanged.
|
||||
- Do not reintroduce `platform_legacy`.
|
||||
|
||||
Done when:
|
||||
|
||||
- Concrete Web platform files are not compiled as app sources in the root app
|
||||
source group.
|
||||
- The source ownership direction is visible in CMake.
|
||||
- Platform API boundary checks still pass.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_platform_api_tests -TestRegex "pp_platform_api"
|
||||
python scripts/dev/check_component_boundaries.py
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Keep this structural. Do not edit platform behavior unless CMake exposes a real
|
||||
link or include problem.
|
||||
|
||||
## P1 Queue
|
||||
|
||||
### ARC-SAFE-010 - Remove Manual Allocation From Touched Ownership Paths
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
The review found manual `new`/`delete` pockets in node loading, canvas, layer
|
||||
actions, Wacom/bootstrap helpers, and retained resource cleanup. Some are
|
||||
non-owning or placement-new cases, but touched working-app ownership paths
|
||||
should move to RAII containers and factories.
|
||||
|
||||
Write scope:
|
||||
|
||||
- one selected ownership path at a time, such as `src/legacy_ui_node_loader.*`,
|
||||
`src/node.*`, `src/canvas.cpp`, `src/platform_windows/windows_bootstrap_helpers.cpp`,
|
||||
or `src/wacom.cpp`
|
||||
|
||||
Read scope:
|
||||
|
||||
- immediate owner/caller files for the selected path
|
||||
|
||||
Required work:
|
||||
|
||||
- Replace owning raw allocation with `std::unique_ptr`, `std::shared_ptr`,
|
||||
`std::vector`, `std::string`, or an explicit RAII wrapper.
|
||||
- Preserve non-owning views only where ownership is proven.
|
||||
- Avoid mixing this with UI or renderer redesign.
|
||||
|
||||
Done when:
|
||||
|
||||
- The touched path has no owning raw `new`/`delete`.
|
||||
- Failure paths cannot leak.
|
||||
- Lifetime remains clear at call sites.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_ui_core|pp_app_core"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Start with a single obvious allocation family. `legacy_ui_node_loader` is a
|
||||
good first target because it is UI ownership, not rendering behavior.
|
||||
|
||||
### ARC-SAFE-011 - Replace Remaining Ad Hoc Workers With Runtime-Owned Services
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Most recent worker conversions use `std::jthread`, but retained worker pockets
|
||||
still sit in UI/dialog/cloud/grid/preview services and `std::async` remains in
|
||||
`Asset`. The end state requires service-owned cancellation and shutdown.
|
||||
|
||||
Write scope:
|
||||
|
||||
- one worker family per slice:
|
||||
`src/node_dialog_cloud.*`, `src/legacy_cloud_services.*`,
|
||||
`src/legacy_grid_ui_services.*`, `src/asset.*`, or
|
||||
`src/legacy_node_stroke_preview_runtime_services.*`
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/app_runtime.*`
|
||||
- corresponding app-core/cloud/grid/preview planner headers
|
||||
- immediate UI caller files
|
||||
|
||||
Required work:
|
||||
|
||||
- Move worker ownership behind a runtime/service contract.
|
||||
- Add cancellation or shutdown semantics for the touched worker.
|
||||
- Avoid capturing raw nodes across worker completion without checked handles.
|
||||
- Preserve progress and completion callbacks.
|
||||
|
||||
Done when:
|
||||
|
||||
- The touched worker cannot outlive its owner.
|
||||
- Shutdown behavior is explicit and validated.
|
||||
- UI completion handoff is handle-safe or owner-checked.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_app_core|pp_ui_core"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Pick one worker family. Do not perform broad thread cleanup across unrelated
|
||||
subsystems in one task.
|
||||
|
||||
### ARC-WKF-010 - Thin Document Session/Open/Save Bridges
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
The pure document/session planners are extensive, but live bridges still own
|
||||
retained prompts, metadata mutation, title updates, history clearing, snapshot
|
||||
handoff, and legacy `Canvas` execution.
|
||||
|
||||
Write scope:
|
||||
|
||||
- `src/legacy_document_open_services.*`
|
||||
- `src/legacy_document_session_services.*`
|
||||
- `src/legacy_history_services.*`
|
||||
- focused app-core document/session headers only when needed
|
||||
|
||||
Read scope:
|
||||
|
||||
- `src/app_core/document_route.h`
|
||||
- `src/app_core/document_session.h`
|
||||
- `src/app_core/document_canvas.h`
|
||||
- `src/app.h`
|
||||
- `src/canvas.*`
|
||||
|
||||
Required work:
|
||||
|
||||
- Pick one bridge path: open-project confirmation, save-before-workflow,
|
||||
save-version, new-document overwrite, history clear, or title update.
|
||||
- Move decisions/mutations behind explicit service requests.
|
||||
- Keep retained prompts as adapters only.
|
||||
|
||||
Done when:
|
||||
|
||||
- One document workflow path has a single obvious owner for decision,
|
||||
execution, and metadata mutation.
|
||||
- The retained bridge has less direct `App::I`/`Canvas::I` reach.
|
||||
- Behavior is covered by existing or focused document-session validation.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_session_tests,pp_app_core_document_route_tests -TestRegex "document_(session|route)"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Do not rewrite all document flows. Pick the narrowest path that removes live
|
||||
bridge ownership.
|
||||
|
||||
### ARC-WKF-011 - Split Cloud And Brush Package Work Out Of UI Nodes
|
||||
|
||||
Status: Ready
|
||||
|
||||
Why now:
|
||||
Cloud browse/download/upload and brush package import/export still mix UI node
|
||||
lifetime, worker ownership, storage, network/asset behavior, and app singleton
|
||||
reach.
|
||||
|
||||
Write scope:
|
||||
|
||||
- one family per slice:
|
||||
`src/legacy_cloud_services.*`, `src/node_dialog_cloud.*`,
|
||||
`src/legacy_brush_package_import_services.*`,
|
||||
`src/legacy_brush_package_export_services.*`,
|
||||
`src/legacy_brush_preset_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.*`
|
||||
- relevant panel/dialog headers
|
||||
|
||||
Required work:
|
||||
|
||||
- Separate worker/network/asset execution from node lifetime.
|
||||
- Use app-core requests and asset helpers where they already exist.
|
||||
- Use checked handles for UI completion callbacks.
|
||||
- Preserve current cloud and brush package UX.
|
||||
|
||||
Done when:
|
||||
|
||||
- One cloud or brush package path can be understood without reading panel or
|
||||
dialog internals first.
|
||||
- The touched UI node is a view/controller shell, not the workflow owner.
|
||||
- The touched worker cannot outlive its service/UI owner.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_cloud_tests,pp_app_core_brush_package_import_tests,pp_app_core_brush_package_export_tests,pp_assets_brush_package_tests -TestRegex "document_cloud|brush_package"
|
||||
```
|
||||
|
||||
Mini-model packet:
|
||||
Cloud and brush package work are separate packets. Do not assign both to one
|
||||
small worker.
|
||||
|
||||
## Deferred On Purpose
|
||||
|
||||
- Vulkan, Metal, WebGPU, and broad future-backend implementation.
|
||||
- OpenXR implementation beyond boundary cleanup needed to remove OpenVR debt.
|
||||
- Package-only migration that does not affect root app architecture.
|
||||
- CLI/planner-only expansion.
|
||||
- Broad warning cleanup without ownership movement.
|
||||
- Documentation-only progress claims.
|
||||
@@ -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
|
||||
@@ -45,6 +43,7 @@ add_executable(panopainter
|
||||
../src/app_layout.cpp
|
||||
../src/app_shaders.cpp
|
||||
../src/app_vr.cpp
|
||||
../src/platform_linux/linux_platform_services.cpp
|
||||
../src/brush.cpp
|
||||
../src/canvas.cpp
|
||||
../src/canvas_layer.cpp
|
||||
@@ -120,4 +119,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>")
|
||||
|
||||
@@ -3,47 +3,11 @@
|
||||
#include <glad/glad.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <app.h>
|
||||
#include <libgen.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
#include <platform_linux/linux_platform_services.h>
|
||||
|
||||
static App app;
|
||||
glm::vec2 g_cursor_pos;
|
||||
|
||||
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE)
|
||||
{
|
||||
struct stat sb;
|
||||
|
||||
if (dir.empty()) {
|
||||
errno = EINVAL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!stat(dir.c_str(), &sb))
|
||||
return 0;
|
||||
|
||||
mkpath(dirname(strdupa(dir.c_str())), mode);
|
||||
|
||||
int ret = mkdir(dir.c_str(), mode);
|
||||
chmod(dir.c_str(), S_IRWXU);
|
||||
if (ret != 0)
|
||||
LOG("mkdir failed with error %d on %s", errno, dir.c_str());
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string linux_home_path()
|
||||
{
|
||||
struct passwd *pw = getpwuid(getuid());
|
||||
return pw->pw_dir;
|
||||
}
|
||||
|
||||
void linux_update_fps(int frames)
|
||||
{
|
||||
static char title_fps[512];
|
||||
sprintf(title_fps, "PanoPainter - %d fps", frames);
|
||||
glfwSetWindowTitle(app.glfw_window, title_fps);
|
||||
}
|
||||
|
||||
void error_log(int code, const char * s)
|
||||
{
|
||||
printf("glfw error: %s", s);
|
||||
@@ -69,6 +33,14 @@ int main(int argc, char** args)
|
||||
printf("could not create window\n");
|
||||
return 1;
|
||||
}
|
||||
pp::platform::linux_desktop::set_fps_title_callback([wnd](std::string title) {
|
||||
glfwSetWindowTitle(wnd, title.c_str());
|
||||
});
|
||||
auto platform_services = pp::platform::linux_desktop::create_platform_services({
|
||||
.acquire_render_context = [wnd] { glfwMakeContextCurrent(wnd); },
|
||||
.present_render_context = [wnd] { glfwSwapBuffers(wnd); },
|
||||
.request_app_close = [wnd] { glfwSetWindowShouldClose(wnd, GLFW_TRUE); },
|
||||
});
|
||||
|
||||
glfwSetCursorPosCallback(wnd, [](GLFWwindow* wnd, double x, double y){
|
||||
g_cursor_pos = glm::vec2(x, y);
|
||||
@@ -90,9 +62,9 @@ int main(int argc, char** args)
|
||||
});
|
||||
});
|
||||
glfwSetWindowCloseCallback(wnd, [](GLFWwindow* wnd){
|
||||
app.ui_task([] {
|
||||
app.ui_task([wnd] {
|
||||
if (!app.request_close())
|
||||
glfwSetWindowShouldClose(app.glfw_window, GLFW_FALSE);
|
||||
glfwSetWindowShouldClose(wnd, GLFW_FALSE);
|
||||
});
|
||||
});
|
||||
glfwSetWindowRefreshCallback(wnd, [](GLFWwindow* wnd){
|
||||
@@ -116,11 +88,11 @@ int main(int argc, char** args)
|
||||
umask(0);
|
||||
|
||||
App::I = &app;
|
||||
app.set_platform_services(platform_services.get());
|
||||
app.initLog();
|
||||
app.create();
|
||||
app.width = 800;
|
||||
app.height = 600;
|
||||
app.glfw_window = wnd;
|
||||
app.render_thread_start();
|
||||
app.ui_thread_start();
|
||||
|
||||
@@ -132,6 +104,7 @@ int main(int argc, char** args)
|
||||
app.ui_thread_stop();
|
||||
app.render_thread_stop();
|
||||
app.terminate();
|
||||
pp::platform::linux_desktop::set_fps_title_callback({});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
167
scripts/automation/apple-remote-build.ps1
Normal file
167
scripts/automation/apple-remote-build.ps1
Normal file
@@ -0,0 +1,167 @@
|
||||
[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"),
|
||||
[switch]$Quiet,
|
||||
[string]$LogDir = "out/logs/apple-remote-build",
|
||||
[int]$FailureTailLines = 0
|
||||
)
|
||||
|
||||
$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
|
||||
|
||||
$quietLiteral = if ($Quiet) { "1" } else { "0" }
|
||||
|
||||
$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
|
||||
quiet_mode=$quietLiteral
|
||||
|
||||
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"
|
||||
if [ "`$quiet_mode" != "1" ]; then
|
||||
tail -n 80 "`$log"
|
||||
fi
|
||||
exit "`$exit_code"
|
||||
"@
|
||||
|
||||
$remoteScript = $remoteScript -replace "`r`n", "`n"
|
||||
$encodedRemoteScript = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($remoteScript))
|
||||
if (-not $Quiet) {
|
||||
& ssh -o BatchMode=yes $HostName "printf '%s' '$encodedRemoteScript' | base64 -D | sh"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$runId = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$logPath = Join-Path -Path $LogDir -ChildPath "$runId-apple-remote-build.log"
|
||||
$stderrPath = Join-Path -Path $LogDir -ChildPath "$runId-apple-remote-build.stderr.log"
|
||||
$started = Get-Date
|
||||
$exitCode = 0
|
||||
try {
|
||||
$process = Start-Process `
|
||||
-FilePath "ssh" `
|
||||
-ArgumentList @("-o", "BatchMode=yes", $HostName, "printf '%s' '$encodedRemoteScript' | base64 -D | sh") `
|
||||
-NoNewWindow `
|
||||
-Wait `
|
||||
-PassThru `
|
||||
-RedirectStandardOutput $logPath `
|
||||
-RedirectStandardError $stderrPath
|
||||
$exitCode = $process.ExitCode
|
||||
}
|
||||
catch {
|
||||
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
|
||||
$exitCode = 1
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $stderrPath) {
|
||||
Get-Content -LiteralPath $stderrPath | Out-File -LiteralPath $logPath -Append -Encoding utf8
|
||||
Remove-Item -LiteralPath $stderrPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
$rawLines = if (Test-Path -LiteralPath $logPath) {
|
||||
@(Get-Content -LiteralPath $logPath)
|
||||
} else {
|
||||
@()
|
||||
}
|
||||
$remoteSummary = $null
|
||||
foreach ($line in $rawLines) {
|
||||
if ($line -match '"command":"apple-remote-build"') {
|
||||
try {
|
||||
$remoteSummary = $line | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [ordered]@{
|
||||
command = "apple-remote-build"
|
||||
exitCode = $exitCode
|
||||
elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
host = $HostName
|
||||
branch = $Branch
|
||||
presets = $Presets
|
||||
log = $logPath
|
||||
}
|
||||
if ($null -ne $remoteSummary) {
|
||||
$payload.remoteHost = $remoteSummary.host
|
||||
$payload.remoteLog = $remoteSummary.log
|
||||
}
|
||||
if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and $rawLines.Count -gt 0) {
|
||||
$payload.failureTail = @($rawLines | Select-Object -Last $FailureTailLines | ForEach-Object { [string]$_ })
|
||||
}
|
||||
|
||||
$payload | ConvertTo-Json -Compress -Depth 6
|
||||
exit $exitCode
|
||||
@@ -3,12 +3,16 @@ 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"
|
||||
)
|
||||
)
|
||||
@@ -22,6 +26,21 @@ function Test-CommandAvailable {
|
||||
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,
|
||||
@@ -80,6 +99,168 @@ function New-PackageReadiness {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -90,18 +271,20 @@ function Get-PackageReadiness {
|
||||
$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 "blocked" `
|
||||
-Status $status `
|
||||
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
|
||||
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
|
||||
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
|
||||
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Prerequisites $prerequisites `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
|
||||
)
|
||||
@@ -110,18 +293,22 @@ function Get-PackageReadiness {
|
||||
$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 "blocked" `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/android assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Prerequisites $prerequisites `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
@@ -130,18 +317,22 @@ function Get-PackageReadiness {
|
||||
$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 "blocked" `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/quest assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Prerequisites $prerequisites `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
@@ -150,18 +341,22 @@ function Get-PackageReadiness {
|
||||
$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 "blocked" `
|
||||
-Status $status `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/focus assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Prerequisites $prerequisites `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
@@ -169,34 +364,62 @@ function Get-PackageReadiness {
|
||||
"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 "blocked" `
|
||||
-Status $status `
|
||||
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
|
||||
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
|
||||
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Prerequisites $prerequisites `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"webgl" {
|
||||
$webDir = Join-Path $root "out/package/webgl"
|
||||
"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 "blocked" `
|
||||
-Reason "emscripten-preset-exists-but-webgl-package-target-missing" `
|
||||
-ValidationCommand "cmake --build --preset emscripten --target PanoPainter" `
|
||||
-Prerequisites @(
|
||||
(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 "root-cmake-preset" -Available $true -Detail "emscripten"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-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")
|
||||
)
|
||||
@@ -207,7 +430,33 @@ function Get-PackageReadiness {
|
||||
return $readiness
|
||||
}
|
||||
|
||||
& cmake --build --preset $Preset --config $Configuration --target $Target
|
||||
$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
|
||||
@@ -217,22 +466,35 @@ if ($buildExitCode -ne 0) {
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "build"
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $buildExitCode
|
||||
elapsedMs = $elapsed
|
||||
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||
} | ConvertTo-Json -Compress
|
||||
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"
|
||||
$dataDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "data"
|
||||
$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 = "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]@{
|
||||
@@ -240,10 +502,12 @@ $elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsedMs
|
||||
checks = $checks
|
||||
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
|
||||
} | ConvertTo-Json -Compress -Depth 5
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
|
||||
exit $exitCode
|
||||
|
||||
@@ -1,25 +1,100 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-PanoPainter}"
|
||||
artifact="${4:-out/build/$preset/$target}"
|
||||
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"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
printf '"%s"' "$(json_escape "$1")"
|
||||
}
|
||||
|
||||
json_bool() {
|
||||
if [ "$1" = "1" ]; then
|
||||
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
|
||||
}
|
||||
@@ -32,76 +107,550 @@ dir_available() {
|
||||
[ -d "$1" ]
|
||||
}
|
||||
|
||||
package_readiness_json() {
|
||||
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 "$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 "$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 "$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")
|
||||
|
||||
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)"
|
||||
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")
|
||||
|
||||
printf '['
|
||||
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
|
||||
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
|
||||
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
|
||||
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
|
||||
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
|
||||
printf ',{"kind":"webgl","status":"blocked","reason":"emscripten-preset-exists-but-webgl-package-target-missing","debt":"DEBT-0011","validationCommand":"cmake --build --preset emscripten --target PanoPainter","prerequisites":[{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
|
||||
printf ']'
|
||||
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"
|
||||
}
|
||||
|
||||
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
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
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness"
|
||||
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
|
||||
|
||||
if [ -e "$artifact" ]; then
|
||||
exit_code=0
|
||||
else
|
||||
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
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness"
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64"),
|
||||
[string[]]$Presets = @("android-arm64", "android-x64", "android-quest-arm64", "android-focus-arm64"),
|
||||
[string[]]$Targets = @(
|
||||
"pp_foundation",
|
||||
"pp_assets",
|
||||
@@ -19,6 +19,8 @@ param(
|
||||
"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",
|
||||
@@ -35,37 +37,224 @@ param(
|
||||
"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_app_core_document_route_tests",
|
||||
"pp_app_core_document_export_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_app_preferences_tests",
|
||||
"pp_app_core_app_status_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_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"
|
||||
),
|
||||
[switch]$Quiet,
|
||||
[string]$LogDir = "out/logs/platform-build",
|
||||
[int]$FailureTailLines = 0
|
||||
)
|
||||
|
||||
$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 Limit-LogSlug {
|
||||
param(
|
||||
[string]$Value,
|
||||
[int]$MaxLength = 96
|
||||
)
|
||||
|
||||
if ($Value.Length -le $MaxLength) {
|
||||
return $Value
|
||||
}
|
||||
return $Value.Substring(0, $MaxLength)
|
||||
}
|
||||
|
||||
function Invoke-LoggedCommand {
|
||||
param(
|
||||
[string]$Command,
|
||||
[string[]]$Arguments,
|
||||
[string]$LogPath,
|
||||
[int]$FailureTailLines
|
||||
)
|
||||
|
||||
$started = Get-Date
|
||||
$exitCode = 0
|
||||
$restoreNativeCommandPreference = $false
|
||||
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
|
||||
$previousNativeCommandPreference = $PSNativeCommandUseErrorActionPreference
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
$restoreNativeCommandPreference = $true
|
||||
}
|
||||
try {
|
||||
& $Command @Arguments *> $LogPath
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($null -eq $exitCode) {
|
||||
$exitCode = 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
|
||||
$exitCode = 1
|
||||
}
|
||||
finally {
|
||||
if ($restoreNativeCommandPreference) {
|
||||
$PSNativeCommandUseErrorActionPreference = $previousNativeCommandPreference
|
||||
}
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
exitCode = $exitCode
|
||||
elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
log = $LogPath
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
$runId = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
|
||||
if ($Quiet) {
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
}
|
||||
|
||||
foreach ($preset in $Presets) {
|
||||
& cmake --preset $preset
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
$presetCmakeCommand = $cmakeCommand
|
||||
if ($androidToolchain -and $preset -notlike "android-*") {
|
||||
$presetCmakeCommand = "cmake"
|
||||
}
|
||||
|
||||
$configureExitCode = 0
|
||||
$configureLog = $null
|
||||
if ($Quiet) {
|
||||
$configureLog = Join-Path -Path $LogDir -ChildPath ("{0}-configure-{1}.log" -f $runId, (Limit-LogSlug (($preset -replace "[^A-Za-z0-9_.-]", "_"))))
|
||||
$configureResult = Invoke-LoggedCommand `
|
||||
-Command $presetCmakeCommand `
|
||||
-Arguments @("--preset", $preset) `
|
||||
-LogPath $configureLog `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$configureExitCode = $configureResult.exitCode
|
||||
}
|
||||
else {
|
||||
& $presetCmakeCommand --preset $preset
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
}
|
||||
if ($configureExitCode -ne 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
$results += [ordered]@{
|
||||
$result = [ordered]@{
|
||||
preset = $preset
|
||||
stage = "configure"
|
||||
exitCode = $configureExitCode
|
||||
}
|
||||
if ($Quiet) {
|
||||
$result.log = $configureLog
|
||||
if ($configureResult.Contains("failureTail")) {
|
||||
$result.failureTail = $configureResult.failureTail
|
||||
}
|
||||
}
|
||||
$results += $result
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -74,18 +263,39 @@ foreach ($preset in $Presets) {
|
||||
$buildArgs += @("--target", $target)
|
||||
}
|
||||
|
||||
& cmake @buildArgs
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
$buildExitCode = 0
|
||||
$buildLog = $null
|
||||
if ($Quiet) {
|
||||
$safeTargets = Limit-LogSlug (($Targets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
|
||||
$buildLog = Join-Path -Path $LogDir -ChildPath ("{0}-build-{1}-{2}.log" -f $runId, ($preset -replace "[^A-Za-z0-9_.-]", "_"), $safeTargets)
|
||||
$buildResult = Invoke-LoggedCommand `
|
||||
-Command $presetCmakeCommand `
|
||||
-Arguments $buildArgs `
|
||||
-LogPath $buildLog `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$buildExitCode = $buildResult.exitCode
|
||||
}
|
||||
else {
|
||||
& $presetCmakeCommand @buildArgs
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
}
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
$result = [ordered]@{
|
||||
preset = $preset
|
||||
stage = "build"
|
||||
targets = $Targets
|
||||
exitCode = $buildExitCode
|
||||
}
|
||||
if ($Quiet) {
|
||||
$result.log = $buildLog
|
||||
if ($buildResult.Contains("failureTail")) {
|
||||
$result.failureTail = $buildResult.failureTail
|
||||
}
|
||||
}
|
||||
$results += $result
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
@@ -93,6 +303,8 @@ $elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
command = "platform-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidToolchain = $androidToolchain
|
||||
vcpkgToolchain = $vcpkgToolchain
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_document_route_tests pp_app_core_document_export_tests pp_app_core_document_cloud_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_app_preferences_tests pp_app_core_app_status_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests}"
|
||||
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)"
|
||||
|
||||
cmake --preset "$preset"
|
||||
configure_exit="$?"
|
||||
if [ "$configure_exit" -ne 0 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms"
|
||||
exit "$configure_exit"
|
||||
fi
|
||||
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
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
cmake --build --preset "$preset" $build_args
|
||||
build_exit="$?"
|
||||
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 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms"
|
||||
exit "$build_exit"
|
||||
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"
|
||||
|
||||
393
scripts/automation/quiet-validate.ps1
Normal file
393
scripts/automation/quiet-validate.ps1
Normal file
@@ -0,0 +1,393 @@
|
||||
[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,
|
||||
[switch]$IncludePlatformBuild,
|
||||
[string[]]$PlatformBuildPresets = @("android-arm64", "android-x64", "android-quest-arm64", "android-focus-arm64"),
|
||||
[string[]]$PlatformBuildTargets = @(),
|
||||
[switch]$IncludeAppleRemote,
|
||||
[string[]]$AppleRemotePresets = @("macos", "ios-simulator", "ios-device")
|
||||
)
|
||||
|
||||
$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
|
||||
$restoreNativeCommandPreference = $false
|
||||
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
|
||||
$previousNativeCommandPreference = $PSNativeCommandUseErrorActionPreference
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
$restoreNativeCommandPreference = $true
|
||||
}
|
||||
try {
|
||||
& $Command @Arguments *> $LogPath
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($null -eq $exitCode) {
|
||||
$exitCode = 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
|
||||
$exitCode = 1
|
||||
}
|
||||
finally {
|
||||
if ($restoreNativeCommandPreference) {
|
||||
$PSNativeCommandUseErrorActionPreference = $previousNativeCommandPreference
|
||||
}
|
||||
}
|
||||
|
||||
$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)
|
||||
$PlatformBuildPresets = @(Expand-ArgumentList -Values $PlatformBuildPresets)
|
||||
$PlatformBuildTargets = @(Expand-ArgumentList -Values $PlatformBuildTargets)
|
||||
$AppleRemotePresets = @(Expand-ArgumentList -Values $AppleRemotePresets)
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
||||
if ($IncludePlatformBuild) {
|
||||
$safePresets = if ($PlatformBuildPresets.Count -gt 0) {
|
||||
Limit-LogSlug -Value (($PlatformBuildPresets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
|
||||
} else {
|
||||
"defaults"
|
||||
}
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-platform-build-$safePresets.log"
|
||||
$platformArgs = @(
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Join-Path -Path $PSScriptRoot -ChildPath "platform-build.ps1"),
|
||||
"-Quiet",
|
||||
"-FailureTailLines", [string]$FailureTailLines
|
||||
)
|
||||
if ($PlatformBuildPresets.Count -gt 0) {
|
||||
$platformArgs += @("-Presets", ($PlatformBuildPresets -join ","))
|
||||
}
|
||||
if ($PlatformBuildTargets.Count -gt 0) {
|
||||
$platformArgs += @("-Targets", ($PlatformBuildTargets -join ","))
|
||||
}
|
||||
$result = Invoke-QuietStep `
|
||||
-Name "platform-build" `
|
||||
-Command "powershell" `
|
||||
-Arguments $platformArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
if ($PlatformBuildPresets.Count -gt 0) {
|
||||
$result.presets = $PlatformBuildPresets
|
||||
}
|
||||
if ($PlatformBuildTargets.Count -gt 0) {
|
||||
$result.targets = $PlatformBuildTargets
|
||||
}
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
if ($IncludeAppleRemote) {
|
||||
$safePresets = if ($AppleRemotePresets.Count -gt 0) {
|
||||
Limit-LogSlug -Value (($AppleRemotePresets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
|
||||
} else {
|
||||
"defaults"
|
||||
}
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-apple-remote-$safePresets.log"
|
||||
$appleArgs = @(
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Join-Path -Path $PSScriptRoot -ChildPath "apple-remote-build.ps1"),
|
||||
"-Quiet",
|
||||
"-FailureTailLines", [string]$FailureTailLines
|
||||
)
|
||||
if ($AppleRemotePresets.Count -gt 0) {
|
||||
$appleArgs += @("-Presets", ($AppleRemotePresets -join ","))
|
||||
}
|
||||
$result = Invoke-QuietStep `
|
||||
-Name "apple-remote-build" `
|
||||
-Command "powershell" `
|
||||
-Arguments $appleArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
if ($AppleRemotePresets.Count -gt 0) {
|
||||
$result.presets = $AppleRemotePresets
|
||||
}
|
||||
$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
|
||||
112
scripts/automation/run-debugger.ps1
Normal file
112
scripts/automation/run-debugger.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BuildPreset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Executable = "",
|
||||
[string]$DebuggerCommand = "",
|
||||
[string]$LogDir = "out/logs/debugger",
|
||||
[int]$StartupSmokeSeconds = 20,
|
||||
[switch]$BreakOnFirstChanceAccessViolation,
|
||||
[switch]$LeaveRunning
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-CdbPath {
|
||||
if ($DebuggerCommand.Length -gt 0) {
|
||||
return $DebuggerCommand
|
||||
}
|
||||
|
||||
$candidates = @(
|
||||
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe",
|
||||
"C:\Program Files\Windows Kits\10\Debuggers\x64\cdb.exe"
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw "Unable to find cdb.exe. Install the Windows Debugging Tools or pass -DebuggerCommand."
|
||||
}
|
||||
|
||||
function Resolve-ExecutablePath {
|
||||
param(
|
||||
[string]$Requested,
|
||||
[string]$Preset,
|
||||
[string]$Config
|
||||
)
|
||||
|
||||
if ($Requested.Length -gt 0) {
|
||||
return (Resolve-Path -LiteralPath $Requested).Path
|
||||
}
|
||||
|
||||
$candidate = Join-Path -Path "out/build/$Preset/$Config" -ChildPath "PanoPainter.exe"
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
|
||||
throw "Unable to find PanoPainter.exe at '$candidate'. Pass -Executable to override."
|
||||
}
|
||||
|
||||
function New-DebuggerCommandFile {
|
||||
param(
|
||||
[string]$Path,
|
||||
[bool]$BreakOnFirstChanceAccessViolation
|
||||
)
|
||||
|
||||
$lines = @()
|
||||
if ($BreakOnFirstChanceAccessViolation) {
|
||||
$lines += 'sxe -c ".echo ==== FIRST CHANCE AV ====; .ecxr; kb; kv; q" av'
|
||||
}
|
||||
$lines += "g"
|
||||
Set-Content -LiteralPath $Path -Value $lines
|
||||
}
|
||||
|
||||
$cdb = Resolve-CdbPath
|
||||
$exe = Resolve-ExecutablePath -Requested $Executable -Preset $BuildPreset -Config $Configuration
|
||||
|
||||
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$commandFile = Join-Path -Path $LogDir -ChildPath "$timestamp-cdb.cmd"
|
||||
$logPath = Join-Path -Path $LogDir -ChildPath "$timestamp-cdb.log"
|
||||
|
||||
New-DebuggerCommandFile -Path $commandFile -BreakOnFirstChanceAccessViolation:$BreakOnFirstChanceAccessViolation
|
||||
|
||||
$process = Start-Process -FilePath $cdb -ArgumentList @(
|
||||
"-lines",
|
||||
"-logo", $logPath,
|
||||
"-cf", $commandFile,
|
||||
$exe
|
||||
) -PassThru
|
||||
|
||||
Start-Sleep -Seconds $StartupSmokeSeconds
|
||||
|
||||
$app = Get-Process PanoPainter -ErrorAction SilentlyContinue
|
||||
$debugger = Get-Process cdb -ErrorAction SilentlyContinue | Where-Object { $_.Id -eq $process.Id }
|
||||
|
||||
$summary = [ordered]@{
|
||||
debugger = $cdb
|
||||
executable = $exe
|
||||
commandFile = $commandFile
|
||||
log = $logPath
|
||||
smokeSeconds = $StartupSmokeSeconds
|
||||
breakOnFirstChanceAccessViolation = [bool]$BreakOnFirstChanceAccessViolation
|
||||
debuggerRunning = [bool]$debugger
|
||||
appRunning = [bool]$app
|
||||
appResponding = if ($app) { [bool]$app.Responding } else { $false }
|
||||
mainWindowTitle = if ($app) { $app.MainWindowTitle } else { "" }
|
||||
}
|
||||
|
||||
$summary | ConvertTo-Json -Compress
|
||||
|
||||
if (-not $LeaveRunning) {
|
||||
if ($app) {
|
||||
Stop-Process -Id $app.Id -Force
|
||||
}
|
||||
if ($debugger) {
|
||||
Stop-Process -Id $debugger.Id -Force
|
||||
}
|
||||
}
|
||||
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."
|
||||
1026
src/app.cpp
1026
src/app.cpp
File diff suppressed because it is too large
Load Diff
191
src/app.h
191
src/app.h
@@ -19,36 +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
|
||||
@@ -74,19 +77,12 @@ struct VRController
|
||||
virtual float get_trigger_value() const { return 1.f; }
|
||||
};
|
||||
|
||||
struct AppTask : public std::packaged_task<void()>
|
||||
struct VrSessionSnapshot
|
||||
{
|
||||
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
|
||||
}
|
||||
bool has_vr = false;
|
||||
bool vr_active = false;
|
||||
std::array<VRController, 2> vr_controllers{};
|
||||
glm::mat4 vr_head{1.0f};
|
||||
};
|
||||
|
||||
class App
|
||||
@@ -97,7 +93,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;
|
||||
@@ -137,7 +133,6 @@ public:
|
||||
std::string doc_dir;
|
||||
std::string doc_filename;
|
||||
bool has_stylus = false;
|
||||
bool has_vr = false;
|
||||
bool vr_controllers_enabled = true;
|
||||
float off_x = 0;
|
||||
float off_y = 0;
|
||||
@@ -148,10 +143,7 @@ public:
|
||||
bool animate = false;
|
||||
bool ui_visible = true;
|
||||
bool ui_rtl = false;
|
||||
bool vr_active = false;
|
||||
bool vr_only = false;
|
||||
VRController vr_controllers[2];
|
||||
glm::mat4 vr_head;
|
||||
float vr_pressure = 1.f;
|
||||
glm::mat4 vr_rot{ 0 };
|
||||
glm::mat4 vr_uirot{ 0 };
|
||||
@@ -163,34 +155,30 @@ public:
|
||||
int idle_ms = 100;
|
||||
pp::platform::PlatformServices* platform_services_ = nullptr;
|
||||
|
||||
#if defined(__IOS__) && defined(__OBJC__)
|
||||
GameViewController* ios_view;
|
||||
AppDelegate* ios_app;
|
||||
#elif defined(__OSX__) && defined(__OBJC__)
|
||||
View* osx_view;
|
||||
AppOSX* osx_app;
|
||||
#elif __LINUX__ || __WEB__
|
||||
GLFWwindow* glfw_window;
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
struct android_app* and_app;
|
||||
struct engine* and_engine;
|
||||
#endif
|
||||
std::string clipboard_get_text();
|
||||
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();
|
||||
@@ -204,9 +192,14 @@ public:
|
||||
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);
|
||||
[[nodiscard]] VrSessionSnapshot vr_session_snapshot() const;
|
||||
void save_prepared_file(
|
||||
std::string path,
|
||||
std::string suggested_name,
|
||||
@@ -303,10 +296,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);
|
||||
@@ -333,16 +322,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();
|
||||
@@ -350,71 +336,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();
|
||||
@@ -422,59 +367,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,9 +1,8 @@
|
||||
#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()
|
||||
{
|
||||
@@ -13,125 +12,29 @@ void App::cloud_upload()
|
||||
has_canvas && Canvas::I->m_newdoc,
|
||||
has_canvas && Canvas::I->m_unsaved);
|
||||
|
||||
switch (plan.action)
|
||||
{
|
||||
case pp::app::CloudUploadAction::unavailable_no_canvas:
|
||||
return;
|
||||
case pp::app::CloudUploadAction::show_save_required_warning:
|
||||
message_box("Warning", "This document needs to be saved before upload.");
|
||||
return;
|
||||
case pp::app::CloudUploadAction::prompt_publish:
|
||||
break;
|
||||
}
|
||||
|
||||
auto upload_thread = [this] {
|
||||
BT_SetTerminate();
|
||||
|
||||
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 (plan.show_progress)
|
||||
pb = show_progress("Export Pano Image", plan.progress_total);
|
||||
|
||||
for (const auto& n : names)
|
||||
{
|
||||
std::string path = data_path + "/" + n;
|
||||
upload(path);
|
||||
|
||||
if (plan.show_progress)
|
||||
pb->increment();
|
||||
}
|
||||
|
||||
if (plan.show_progress)
|
||||
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()
|
||||
{
|
||||
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
|
||||
switch (browse_plan)
|
||||
{
|
||||
case pp::app::CloudBrowseAction::unavailable_no_canvas:
|
||||
return;
|
||||
case pp::app::CloudBrowseAction::show_browser:
|
||||
break;
|
||||
}
|
||||
|
||||
// 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*)
|
||||
{
|
||||
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
|
||||
if (selection_plan == pp::app::CloudDownloadSelectionAction::wait_for_selection)
|
||||
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 status = pp::panopainter::execute_legacy_cloud_browse_action(*this, browse_plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud browse action failed: %s", status.message);
|
||||
}
|
||||
|
||||
@@ -1,46 +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 {
|
||||
|
||||
[[nodiscard]] GLenum depth_test_state() noexcept
|
||||
void apply_convert_command_state()
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::depth_test_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);
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum program_point_size_state() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::program_point_size_state());
|
||||
}
|
||||
class LegacyCommandConvertServices final : public pp::app::CommandConvertServices {
|
||||
public:
|
||||
void apply_renderer_state() override
|
||||
{
|
||||
apply_convert_command_state();
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum source_alpha_blend_factor() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::source_alpha_blend_factor());
|
||||
}
|
||||
void create_canvas(int canvas_resolution) override
|
||||
{
|
||||
command_canvas = new Canvas;
|
||||
command_canvas->create(canvas_resolution, canvas_resolution);
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum one_minus_source_alpha_blend_factor() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::one_minus_source_alpha_blend_factor());
|
||||
}
|
||||
void open_project(std::string_view project_path) override
|
||||
{
|
||||
if (command_canvas)
|
||||
command_canvas->project_open_thread(std::string(project_path));
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum add_blend_equation() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::add_blend_equation());
|
||||
}
|
||||
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(depth_test_state());
|
||||
glEnable(program_point_size_state());
|
||||
glBlendFunc(source_alpha_blend_factor(), one_minus_source_alpha_blend_factor());
|
||||
glBlendEquation(add_blend_equation());
|
||||
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* command_canvas = new Canvas;
|
||||
command_canvas->create(CANVAS_RES, CANVAS_RES);
|
||||
command_canvas->project_open_thread(pano_path);
|
||||
command_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);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
@@ -44,6 +46,19 @@ 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
|
||||
@@ -106,9 +121,75 @@ struct StoredBooleanPreferencePlan {
|
||||
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
|
||||
@@ -33,6 +33,23 @@ struct RecordingFrameLabel {
|
||||
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()) {
|
||||
@@ -116,4 +133,38 @@ struct RecordingFrameLabel {
|
||||
};
|
||||
}
|
||||
|
||||
[[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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
243
src/app_core/app_thread.h
Normal file
243
src/app_core/app_thread.h
Normal file
@@ -0,0 +1,243 @@
|
||||
#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 AppRuntimeTaskDispatchPlan {
|
||||
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;
|
||||
bool reject_unsafe_cross_thread_dispatch = false;
|
||||
bool reject_stopped_worker_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 AppRuntimeTaskDispatchPlan plan_app_runtime_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
|
||||
&& worker_running
|
||||
&& !reject_unsafe_cross_thread_dispatch;
|
||||
return AppRuntimeTaskDispatchPlan {
|
||||
.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 && wait_for_completion,
|
||||
.request_redraw = (already_on_target_thread || queue_task) && request_redraw_after_dispatch,
|
||||
.reject_unsafe_cross_thread_dispatch = !already_on_target_thread
|
||||
&& reject_unsafe_cross_thread_dispatch,
|
||||
.reject_stopped_worker_dispatch = !already_on_target_thread && !worker_running,
|
||||
};
|
||||
}
|
||||
|
||||
[[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
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
@@ -28,6 +30,14 @@ enum class BrushTextureListOperation {
|
||||
move_texture,
|
||||
};
|
||||
|
||||
enum class BrushPresetListOperation {
|
||||
add_current_brush,
|
||||
remove_preset,
|
||||
move_preset,
|
||||
select_preset,
|
||||
clear_presets,
|
||||
};
|
||||
|
||||
enum class BrushStrokeControlOperation {
|
||||
set_float,
|
||||
set_bool,
|
||||
@@ -139,6 +149,20 @@ struct BrushTextureListPlan {
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
struct BrushPresetListPlan {
|
||||
BrushPresetListOperation operation = BrushPresetListOperation::select_preset;
|
||||
int item_count = 0;
|
||||
int current_index = -1;
|
||||
int target_index = -1;
|
||||
int move_offset = 0;
|
||||
bool saves_list = false;
|
||||
bool updates_empty_notification = false;
|
||||
bool selects_target = false;
|
||||
bool clears_selection = false;
|
||||
bool notifies_brush_changed = false;
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
struct BrushStrokeControlPlan {
|
||||
BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
|
||||
BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
|
||||
@@ -153,6 +177,72 @@ struct BrushStrokeControlPlan {
|
||||
bool notifies_stroke_change = false;
|
||||
};
|
||||
|
||||
struct BrushStrokeFloatValue {
|
||||
BrushStrokeFloatSetting setting = BrushStrokeFloatSetting::tip_size;
|
||||
float value = 0.0F;
|
||||
};
|
||||
|
||||
struct BrushStrokeBoolValue {
|
||||
BrushStrokeBoolSetting setting = BrushStrokeBoolSetting::tip_angle_init;
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
struct BrushStrokeBlendValue {
|
||||
BrushStrokeBlendSetting setting = BrushStrokeBlendSetting::tip;
|
||||
int blend_mode = 0;
|
||||
};
|
||||
|
||||
struct BrushStrokePanelInput {
|
||||
std::vector<BrushStrokeFloatValue> float_values;
|
||||
std::vector<BrushStrokeBoolValue> bool_values;
|
||||
std::vector<BrushStrokeBlendValue> blend_values;
|
||||
std::string tip_thumbnail_path;
|
||||
std::string dual_thumbnail_path;
|
||||
std::string pattern_thumbnail_path;
|
||||
};
|
||||
|
||||
struct BrushStrokePanelView {
|
||||
std::vector<BrushStrokeFloatValue> float_values;
|
||||
std::vector<BrushStrokeBoolValue> bool_values;
|
||||
std::vector<BrushStrokeBlendValue> blend_values;
|
||||
std::string tip_thumbnail_path;
|
||||
std::string dual_thumbnail_path;
|
||||
std::string pattern_thumbnail_path;
|
||||
bool updates_preview = true;
|
||||
bool updates_thumbnails = true;
|
||||
};
|
||||
|
||||
struct BrushUiRefreshInput {
|
||||
bool update_color = false;
|
||||
bool update_brush = false;
|
||||
bool has_current_brush = true;
|
||||
bool has_floating_picker = false;
|
||||
bool has_floating_color_panel = false;
|
||||
float tip_flow = 0.0F;
|
||||
float tip_size = 0.0F;
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 1.0F;
|
||||
};
|
||||
|
||||
struct BrushUiRefreshView {
|
||||
bool updates_stroke_controls = false;
|
||||
bool updates_quick_flow = false;
|
||||
bool updates_quick_size = false;
|
||||
bool updates_quick_brush_preview = false;
|
||||
bool updates_quick_color = false;
|
||||
bool updates_floating_picker = false;
|
||||
bool updates_floating_color_panel = false;
|
||||
bool no_op = false;
|
||||
float tip_flow = 0.0F;
|
||||
float tip_size = 0.0F;
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 1.0F;
|
||||
};
|
||||
|
||||
class BrushUiServices {
|
||||
public:
|
||||
virtual ~BrushUiServices() = default;
|
||||
@@ -179,6 +269,23 @@ public:
|
||||
virtual void save_texture_list() = 0;
|
||||
};
|
||||
|
||||
class BrushPresetListServices {
|
||||
public:
|
||||
virtual ~BrushPresetListServices() = default;
|
||||
|
||||
virtual pp::foundation::Status add_current_brush_preset(int target_index) = 0;
|
||||
virtual void remove_brush_preset(
|
||||
int current_index,
|
||||
int target_index,
|
||||
bool selects_target,
|
||||
bool clears_selection) = 0;
|
||||
virtual void move_brush_preset(int from_index, int to_index) = 0;
|
||||
virtual void select_brush_preset(int index, bool notify_brush_changed) = 0;
|
||||
virtual void clear_brush_presets(bool clears_selection) = 0;
|
||||
virtual void update_preset_empty_notification() = 0;
|
||||
virtual void save_preset_list() = 0;
|
||||
};
|
||||
|
||||
class BrushStrokeControlServices {
|
||||
public:
|
||||
virtual ~BrushStrokeControlServices() = default;
|
||||
@@ -239,6 +346,84 @@ public:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushStrokePanelView> plan_brush_stroke_panel_view(
|
||||
BrushStrokePanelInput input)
|
||||
{
|
||||
for (const auto& value : input.float_values) {
|
||||
const auto status = validate_brush_stroke_float(value.value);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<BrushStrokePanelView>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& value : input.blend_values) {
|
||||
const auto status = validate_brush_stroke_blend_mode(value.blend_mode);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<BrushStrokePanelView>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
BrushStrokePanelView view;
|
||||
view.float_values = std::move(input.float_values);
|
||||
view.bool_values = std::move(input.bool_values);
|
||||
view.blend_values = std::move(input.blend_values);
|
||||
view.tip_thumbnail_path = std::move(input.tip_thumbnail_path);
|
||||
view.dual_thumbnail_path = std::move(input.dual_thumbnail_path);
|
||||
view.pattern_thumbnail_path = std::move(input.pattern_thumbnail_path);
|
||||
return pp::foundation::Result<BrushStrokePanelView>::success(std::move(view));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiRefreshView> plan_brush_ui_refresh(
|
||||
const BrushUiRefreshInput& input)
|
||||
{
|
||||
if (!input.update_color && !input.update_brush) {
|
||||
BrushUiRefreshView view;
|
||||
view.no_op = true;
|
||||
return pp::foundation::Result<BrushUiRefreshView>::success(view);
|
||||
}
|
||||
|
||||
if (!input.has_current_brush) {
|
||||
return pp::foundation::Result<BrushUiRefreshView>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush refresh requires a current brush"));
|
||||
}
|
||||
|
||||
if (input.update_brush) {
|
||||
const auto flow_status = validate_brush_stroke_float(input.tip_flow);
|
||||
if (!flow_status.ok()) {
|
||||
return pp::foundation::Result<BrushUiRefreshView>::failure(flow_status);
|
||||
}
|
||||
const auto size_status = validate_brush_stroke_float(input.tip_size);
|
||||
if (!size_status.ok()) {
|
||||
return pp::foundation::Result<BrushUiRefreshView>::failure(size_status);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.update_color) {
|
||||
for (const auto value : { input.r, input.g, input.b, input.a }) {
|
||||
const auto channel_status = validate_brush_ui_color_channel(value);
|
||||
if (!channel_status.ok()) {
|
||||
return pp::foundation::Result<BrushUiRefreshView>::failure(channel_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BrushUiRefreshView view;
|
||||
view.updates_stroke_controls = input.update_brush;
|
||||
view.updates_quick_flow = input.update_brush;
|
||||
view.updates_quick_size = input.update_brush;
|
||||
view.updates_quick_brush_preview = input.update_brush;
|
||||
view.updates_quick_color = input.update_color;
|
||||
view.updates_floating_picker = input.update_color && input.has_floating_picker;
|
||||
view.updates_floating_color_panel = input.update_color && input.has_floating_color_panel;
|
||||
view.tip_flow = input.tip_flow;
|
||||
view.tip_size = input.tip_size;
|
||||
view.r = input.r;
|
||||
view.g = input.g;
|
||||
view.b = input.b;
|
||||
view.a = input.a;
|
||||
return pp::foundation::Result<BrushUiRefreshView>::success(view);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
|
||||
float r,
|
||||
float g,
|
||||
@@ -474,6 +659,122 @@ public:
|
||||
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_add(
|
||||
int item_count,
|
||||
bool has_current_brush)
|
||||
{
|
||||
if (item_count < 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
|
||||
}
|
||||
if (!has_current_brush) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("current brush must be available to add a preset"));
|
||||
}
|
||||
|
||||
BrushPresetListPlan plan;
|
||||
plan.operation = BrushPresetListOperation::add_current_brush;
|
||||
plan.item_count = item_count;
|
||||
plan.target_index = item_count;
|
||||
plan.saves_list = true;
|
||||
plan.updates_empty_notification = true;
|
||||
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_remove(
|
||||
int item_count,
|
||||
int current_index)
|
||||
{
|
||||
if (item_count <= 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush preset list must contain an item to remove"));
|
||||
}
|
||||
if (current_index < 0 || current_index >= item_count) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
||||
}
|
||||
|
||||
BrushPresetListPlan plan;
|
||||
plan.operation = BrushPresetListOperation::remove_preset;
|
||||
plan.item_count = item_count;
|
||||
plan.current_index = current_index;
|
||||
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
|
||||
plan.saves_list = true;
|
||||
plan.updates_empty_notification = true;
|
||||
plan.selects_target = plan.target_index >= 0;
|
||||
plan.clears_selection = plan.target_index < 0;
|
||||
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_move(
|
||||
int item_count,
|
||||
int current_index,
|
||||
int offset)
|
||||
{
|
||||
if (item_count <= 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush preset list must contain an item to move"));
|
||||
}
|
||||
if (current_index < 0 || current_index >= item_count) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
||||
}
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush preset move offset must not be zero"));
|
||||
}
|
||||
|
||||
BrushPresetListPlan plan;
|
||||
plan.operation = BrushPresetListOperation::move_preset;
|
||||
plan.item_count = item_count;
|
||||
plan.current_index = current_index;
|
||||
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
|
||||
plan.move_offset = offset;
|
||||
plan.saves_list = true;
|
||||
plan.no_op = plan.target_index == current_index;
|
||||
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_select(
|
||||
int item_count,
|
||||
int index)
|
||||
{
|
||||
if (item_count <= 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush preset list must contain an item to select"));
|
||||
}
|
||||
if (index < 0 || index >= item_count) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
|
||||
}
|
||||
|
||||
BrushPresetListPlan plan;
|
||||
plan.operation = BrushPresetListOperation::select_preset;
|
||||
plan.item_count = item_count;
|
||||
plan.current_index = index;
|
||||
plan.target_index = index;
|
||||
plan.selects_target = true;
|
||||
plan.notifies_brush_changed = true;
|
||||
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_clear(int item_count)
|
||||
{
|
||||
if (item_count < 0) {
|
||||
return pp::foundation::Result<BrushPresetListPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
|
||||
}
|
||||
|
||||
BrushPresetListPlan plan;
|
||||
plan.operation = BrushPresetListOperation::clear_presets;
|
||||
plan.item_count = item_count;
|
||||
plan.saves_list = true;
|
||||
plan.updates_empty_notification = true;
|
||||
plan.clears_selection = true;
|
||||
plan.no_op = item_count == 0;
|
||||
return pp::foundation::Result<BrushPresetListPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
|
||||
const BrushUiPlan& plan,
|
||||
BrushUiServices& services)
|
||||
@@ -625,4 +926,83 @@ public:
|
||||
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_preset_list_plan(
|
||||
const BrushPresetListPlan& plan,
|
||||
BrushPresetListServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case BrushPresetListOperation::add_current_brush:
|
||||
{
|
||||
if (plan.item_count < 0 || plan.target_index != plan.item_count) {
|
||||
return pp::foundation::Status::out_of_range("brush preset add plan has invalid target");
|
||||
}
|
||||
|
||||
const auto add_status = services.add_current_brush_preset(plan.target_index);
|
||||
if (!add_status.ok()) {
|
||||
return add_status;
|
||||
}
|
||||
if (plan.updates_empty_notification) {
|
||||
services.update_preset_empty_notification();
|
||||
}
|
||||
if (plan.saves_list) {
|
||||
services.save_preset_list();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
case BrushPresetListOperation::remove_preset:
|
||||
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
|
||||
return pp::foundation::Status::out_of_range("brush preset remove plan has invalid selection");
|
||||
}
|
||||
if (plan.selects_target && (plan.target_index < 0 || plan.target_index >= plan.item_count - 1)) {
|
||||
return pp::foundation::Status::out_of_range("brush preset remove plan has invalid target");
|
||||
}
|
||||
services.remove_brush_preset(
|
||||
plan.current_index,
|
||||
plan.target_index,
|
||||
plan.selects_target,
|
||||
plan.clears_selection);
|
||||
if (plan.updates_empty_notification) {
|
||||
services.update_preset_empty_notification();
|
||||
}
|
||||
if (plan.saves_list) {
|
||||
services.save_preset_list();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case BrushPresetListOperation::move_preset:
|
||||
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|
||||
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
||||
return pp::foundation::Status::out_of_range("brush preset move plan has invalid indices");
|
||||
}
|
||||
services.move_brush_preset(plan.current_index, plan.target_index);
|
||||
if (plan.saves_list) {
|
||||
services.save_preset_list();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case BrushPresetListOperation::select_preset:
|
||||
if (plan.item_count <= 0 || plan.target_index < 0 || plan.target_index >= plan.item_count) {
|
||||
return pp::foundation::Status::out_of_range("brush preset select plan has invalid target");
|
||||
}
|
||||
services.select_brush_preset(plan.target_index, plan.notifies_brush_changed);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case BrushPresetListOperation::clear_presets:
|
||||
if (plan.item_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("brush preset clear plan has invalid item count");
|
||||
}
|
||||
services.clear_brush_presets(plan.clears_selection);
|
||||
if (plan.updates_empty_notification) {
|
||||
services.update_preset_empty_notification();
|
||||
}
|
||||
if (plan.saves_list) {
|
||||
services.save_preset_list();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown brush preset list operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
|
||||
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
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasToolOperation {
|
||||
@@ -30,6 +34,19 @@ enum class CanvasToolTransformAction {
|
||||
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;
|
||||
@@ -59,6 +76,38 @@ struct CanvasToolButtonState {
|
||||
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;
|
||||
@@ -111,6 +160,44 @@ public:
|
||||
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,
|
||||
@@ -134,6 +221,54 @@ public:
|
||||
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)
|
||||
|
||||
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
|
||||
@@ -3,8 +3,13 @@
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
@@ -62,6 +67,56 @@ struct DocumentAnimationOperationPlan {
|
||||
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;
|
||||
@@ -122,6 +177,167 @@ public:
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
#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 {
|
||||
|
||||
@@ -17,6 +27,129 @@ struct DocumentCanvasClearPlan {
|
||||
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;
|
||||
@@ -35,6 +168,314 @@ public:
|
||||
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,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#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 {
|
||||
@@ -22,6 +28,17 @@ enum class CloudDownloadSelectionAction {
|
||||
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;
|
||||
@@ -33,6 +50,92 @@ struct CloudBulkUploadPlan {
|
||||
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,
|
||||
@@ -76,4 +179,137 @@ struct CloudBulkUploadPlan {
|
||||
};
|
||||
}
|
||||
|
||||
[[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,7 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_canvas.h"
|
||||
#include "document/document.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -22,10 +28,48 @@ struct DocumentExportStemTarget {
|
||||
std::string stem_path;
|
||||
};
|
||||
|
||||
struct DocumentCubeFaceExportFileTarget {
|
||||
std::string face_name;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentCubeFaceExportTarget {
|
||||
std::array<DocumentCubeFaceExportFileTarget, pp::document::cube_face_count> faces;
|
||||
std::size_t face_count = 0;
|
||||
};
|
||||
|
||||
struct DocumentDepthExportTarget {
|
||||
std::string image_path;
|
||||
std::string depth_path;
|
||||
};
|
||||
|
||||
struct DocumentCubeFaceExportPayload {
|
||||
std::span<const std::byte> bytes;
|
||||
};
|
||||
|
||||
struct DocumentDepthExportPayload {
|
||||
std::span<const std::byte> image_bytes;
|
||||
std::span<const std::byte> depth_bytes;
|
||||
};
|
||||
|
||||
struct DocumentExportFilePayload {
|
||||
std::span<const std::byte> bytes;
|
||||
};
|
||||
|
||||
struct DocumentExportCollectionPngPayload {
|
||||
std::string path_suffix;
|
||||
std::span<const std::byte> bytes;
|
||||
};
|
||||
|
||||
struct DocumentExportSuggestedName {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
enum class DocumentExportCollectionDestination {
|
||||
work_directory_collection,
|
||||
picked_directory_stem,
|
||||
};
|
||||
|
||||
enum class DocumentExportStartDecision {
|
||||
start_now,
|
||||
show_license_disabled,
|
||||
@@ -56,12 +100,82 @@ enum class DocumentExportMenuAction {
|
||||
unavailable_no_canvas,
|
||||
};
|
||||
|
||||
enum class DocumentExportCollectionKind {
|
||||
layers,
|
||||
animation_frames,
|
||||
};
|
||||
|
||||
enum class DocumentVideoExportKind {
|
||||
animation_mp4,
|
||||
timelapse,
|
||||
};
|
||||
|
||||
enum class DocumentExportSuccessKind {
|
||||
equirectangular,
|
||||
layers,
|
||||
animation_frames,
|
||||
depth,
|
||||
cube_faces,
|
||||
animation_mp4,
|
||||
timelapse,
|
||||
};
|
||||
|
||||
enum class DocumentExportSuccessDestination {
|
||||
suppressed,
|
||||
photos,
|
||||
pictures_panopainter,
|
||||
files_panopainter,
|
||||
work_directory,
|
||||
path,
|
||||
generic_success,
|
||||
};
|
||||
|
||||
enum class DocumentExportExecutionKind {
|
||||
equirectangular_file,
|
||||
layers_collection,
|
||||
layers_stem,
|
||||
animation_frames_collection,
|
||||
animation_frames_stem,
|
||||
depth,
|
||||
cube_faces,
|
||||
animation_mp4,
|
||||
timelapse,
|
||||
};
|
||||
|
||||
enum class DocumentExportSnapshotRouteAction {
|
||||
use_document_snapshot_writer,
|
||||
use_legacy_export,
|
||||
};
|
||||
|
||||
struct DocumentExportMenuPlan {
|
||||
DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg;
|
||||
DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog;
|
||||
bool opens_dialog = true;
|
||||
};
|
||||
|
||||
struct DocumentExportCollectionTargetPlan {
|
||||
DocumentExportCollectionKind kind = DocumentExportCollectionKind::layers;
|
||||
DocumentExportCollectionDestination destination = DocumentExportCollectionDestination::picked_directory_stem;
|
||||
std::string_view suffix;
|
||||
};
|
||||
|
||||
struct DocumentExportSuccessDialogPlan {
|
||||
DocumentExportSuccessKind kind = DocumentExportSuccessKind::equirectangular;
|
||||
DocumentExportSuccessDestination destination = DocumentExportSuccessDestination::suppressed;
|
||||
AppMessageDialogPlan dialog;
|
||||
bool show_dialog = false;
|
||||
};
|
||||
|
||||
struct DocumentExportSnapshotRoutePlan {
|
||||
DocumentExportExecutionKind kind = DocumentExportExecutionKind::equirectangular_file;
|
||||
DocumentExportSnapshotRouteAction action = DocumentExportSnapshotRouteAction::use_legacy_export;
|
||||
bool payload_complete = false;
|
||||
bool target_supported = false;
|
||||
bool platform_supported = false;
|
||||
bool uses_document_snapshot_writer = false;
|
||||
std::string_view fallback_reason;
|
||||
};
|
||||
|
||||
class DocumentExportMenuServices {
|
||||
public:
|
||||
virtual ~DocumentExportMenuServices() = default;
|
||||
@@ -77,6 +191,70 @@ public:
|
||||
virtual void show_license_disabled() = 0;
|
||||
};
|
||||
|
||||
class DocumentExportServices {
|
||||
public:
|
||||
virtual ~DocumentExportServices() = default;
|
||||
|
||||
virtual bool create_directory(std::string_view directory) = 0;
|
||||
virtual void export_equirectangular(const DocumentExportFileTarget& target) = 0;
|
||||
virtual void export_layers_to_stem(const DocumentExportStemTarget& target) = 0;
|
||||
virtual void export_layers_to_collection(const DocumentExportCollectionTarget& target) = 0;
|
||||
virtual void export_animation_frames_to_stem(const DocumentExportStemTarget& target) = 0;
|
||||
virtual void export_animation_frames_to_collection(const DocumentExportCollectionTarget& target) = 0;
|
||||
virtual void export_depth(std::string_view document_name) = 0;
|
||||
virtual void export_cube_faces(std::string_view document_name) = 0;
|
||||
};
|
||||
|
||||
class DocumentVideoExportServices {
|
||||
public:
|
||||
virtual ~DocumentVideoExportServices() = default;
|
||||
|
||||
virtual void export_animation_mp4(std::string_view path) = 0;
|
||||
virtual void export_timelapse_mp4(std::string_view path) = 0;
|
||||
virtual void show_animation_export_success(std::string_view path) = 0;
|
||||
virtual void show_timelapse_export_success(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
class DocumentCubeFaceExportWriteServices {
|
||||
public:
|
||||
virtual ~DocumentCubeFaceExportWriteServices() = default;
|
||||
|
||||
virtual pp::foundation::Status write_binary_file(
|
||||
std::string_view path,
|
||||
std::span<const std::byte> bytes) = 0;
|
||||
virtual void publish_exported_image(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
class DocumentDepthExportWriteServices {
|
||||
public:
|
||||
virtual ~DocumentDepthExportWriteServices() = default;
|
||||
|
||||
virtual pp::foundation::Status write_binary_file(
|
||||
std::string_view path,
|
||||
std::span<const std::byte> bytes) = 0;
|
||||
virtual void publish_exported_image(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
class DocumentExportFileWriteServices {
|
||||
public:
|
||||
virtual ~DocumentExportFileWriteServices() = default;
|
||||
|
||||
virtual pp::foundation::Status write_binary_file(
|
||||
std::string_view path,
|
||||
std::span<const std::byte> bytes) = 0;
|
||||
virtual void publish_exported_image(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
class DocumentExportCollectionWriteServices {
|
||||
public:
|
||||
virtual ~DocumentExportCollectionWriteServices() = default;
|
||||
|
||||
virtual pp::foundation::Status write_binary_file(
|
||||
std::string_view path,
|
||||
std::span<const std::byte> bytes) = 0;
|
||||
virtual void publish_exported_image(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
|
||||
bool requires_license,
|
||||
bool license_valid,
|
||||
@@ -135,6 +313,32 @@ public:
|
||||
return DocumentExportMenuAction::show_jpeg_dialog;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSuccessDestination document_export_equirectangular_platform_destination() noexcept
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
return DocumentExportSuccessDestination::photos;
|
||||
#elif defined(__OSX__)
|
||||
return DocumentExportSuccessDestination::pictures_panopainter;
|
||||
#elif defined(_WIN32)
|
||||
return DocumentExportSuccessDestination::work_directory;
|
||||
#else
|
||||
return DocumentExportSuccessDestination::suppressed;
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSuccessDestination document_export_media_platform_destination() noexcept
|
||||
{
|
||||
#if defined(__IOS__)
|
||||
return DocumentExportSuccessDestination::files_panopainter;
|
||||
#elif defined(__OSX__)
|
||||
return DocumentExportSuccessDestination::pictures_panopainter;
|
||||
#elif defined(_WIN32)
|
||||
return DocumentExportSuccessDestination::work_directory;
|
||||
#else
|
||||
return DocumentExportSuccessDestination::suppressed;
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action(
|
||||
DocumentExportMenuKind kind,
|
||||
bool has_canvas,
|
||||
@@ -159,6 +363,336 @@ public:
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr std::string_view document_export_collection_suffix(
|
||||
DocumentExportCollectionKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentExportCollectionKind::layers:
|
||||
return "_layers";
|
||||
case DocumentExportCollectionKind::animation_frames:
|
||||
return "_frames";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSuccessKind document_export_collection_success_kind(
|
||||
DocumentExportCollectionKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentExportCollectionKind::layers:
|
||||
return DocumentExportSuccessKind::layers;
|
||||
case DocumentExportCollectionKind::animation_frames:
|
||||
return DocumentExportSuccessKind::animation_frames;
|
||||
}
|
||||
|
||||
return DocumentExportSuccessKind::layers;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const char* document_export_failure_dialog_title(
|
||||
DocumentExportSuccessKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentExportSuccessKind::equirectangular:
|
||||
return "Export Equirectangular";
|
||||
case DocumentExportSuccessKind::layers:
|
||||
case DocumentExportSuccessKind::animation_frames:
|
||||
return "Export Layers";
|
||||
case DocumentExportSuccessKind::depth:
|
||||
return "Export 3D View + Depth";
|
||||
case DocumentExportSuccessKind::cube_faces:
|
||||
return "Export Cube Faces";
|
||||
case DocumentExportSuccessKind::animation_mp4:
|
||||
return "Export Animation";
|
||||
case DocumentExportSuccessKind::timelapse:
|
||||
return "Export Timelapse";
|
||||
}
|
||||
|
||||
return "Export";
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const char* document_export_execution_log_message(
|
||||
DocumentExportExecutionKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentExportExecutionKind::equirectangular_file:
|
||||
return "Document export file action failed";
|
||||
case DocumentExportExecutionKind::layers_collection:
|
||||
return "Document layer collection export failed";
|
||||
case DocumentExportExecutionKind::layers_stem:
|
||||
return "Document layer stem export failed";
|
||||
case DocumentExportExecutionKind::animation_frames_collection:
|
||||
return "Document animation frame collection export failed";
|
||||
case DocumentExportExecutionKind::animation_frames_stem:
|
||||
return "Document animation frame stem export failed";
|
||||
case DocumentExportExecutionKind::depth:
|
||||
return "Document depth export failed";
|
||||
case DocumentExportExecutionKind::cube_faces:
|
||||
return "Document cube-face export failed";
|
||||
case DocumentExportExecutionKind::animation_mp4:
|
||||
return "Document animation export failed";
|
||||
case DocumentExportExecutionKind::timelapse:
|
||||
return "Document timelapse export failed";
|
||||
}
|
||||
|
||||
return "Document export failed";
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool ascii_iequals(std::string_view left, std::string_view right) noexcept
|
||||
{
|
||||
if (left.size() != right.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < left.size(); ++i) {
|
||||
auto lhs = left[i];
|
||||
if (lhs >= 'A' && lhs <= 'Z') {
|
||||
lhs = static_cast<char>(lhs - 'A' + 'a');
|
||||
}
|
||||
auto rhs = right[i];
|
||||
if (rhs >= 'A' && rhs <= 'Z') {
|
||||
rhs = static_cast<char>(rhs - 'A' + 'a');
|
||||
}
|
||||
if (lhs != rhs) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool document_export_path_has_extension(
|
||||
std::string_view path,
|
||||
std::string_view extension) noexcept
|
||||
{
|
||||
return path.size() >= extension.size()
|
||||
&& ascii_iequals(path.substr(path.size() - extension.size()), extension);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool document_export_path_is_png_target(std::string_view path) noexcept
|
||||
{
|
||||
return document_export_path_has_extension(path, ".png");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool document_export_path_is_jpeg_target(std::string_view path) noexcept
|
||||
{
|
||||
return document_export_path_has_extension(path, ".jpg")
|
||||
|| document_export_path_has_extension(path, ".jpeg");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool document_export_snapshot_target_supported(
|
||||
DocumentExportExecutionKind kind,
|
||||
std::string_view target_path = {}) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentExportExecutionKind::equirectangular_file:
|
||||
return document_export_path_is_png_target(target_path)
|
||||
|| document_export_path_is_jpeg_target(target_path);
|
||||
case DocumentExportExecutionKind::layers_collection:
|
||||
case DocumentExportExecutionKind::layers_stem:
|
||||
case DocumentExportExecutionKind::animation_frames_collection:
|
||||
case DocumentExportExecutionKind::animation_frames_stem:
|
||||
case DocumentExportExecutionKind::depth:
|
||||
case DocumentExportExecutionKind::cube_faces:
|
||||
return true;
|
||||
case DocumentExportExecutionKind::animation_mp4:
|
||||
case DocumentExportExecutionKind::timelapse:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool document_export_snapshot_platform_supported() noexcept
|
||||
{
|
||||
#if __WEB__
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route(
|
||||
DocumentExportExecutionKind kind,
|
||||
DocumentCanvasSaveSnapshotReport report,
|
||||
bool target_supported,
|
||||
bool platform_supported) noexcept
|
||||
{
|
||||
DocumentExportSnapshotRoutePlan plan;
|
||||
plan.kind = kind;
|
||||
plan.payload_complete = report.payload_complete;
|
||||
plan.target_supported = target_supported;
|
||||
plan.platform_supported = platform_supported;
|
||||
|
||||
if (!platform_supported) {
|
||||
plan.fallback_reason = "document snapshot export is disabled on this platform";
|
||||
return plan;
|
||||
}
|
||||
|
||||
if (!target_supported) {
|
||||
plan.fallback_reason = "document snapshot export does not support this target";
|
||||
return plan;
|
||||
}
|
||||
|
||||
if (!report.payload_complete) {
|
||||
plan.fallback_reason = "document snapshot still requires renderer payload readback";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.action = DocumentExportSnapshotRouteAction::use_document_snapshot_writer;
|
||||
plan.uses_document_snapshot_writer = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route_for_target(
|
||||
DocumentExportExecutionKind kind,
|
||||
DocumentCanvasSaveSnapshotReport report,
|
||||
std::string_view target_path,
|
||||
bool platform_supported) noexcept
|
||||
{
|
||||
return plan_document_export_snapshot_route(
|
||||
kind,
|
||||
report,
|
||||
document_export_snapshot_target_supported(kind, target_path),
|
||||
platform_supported);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportSnapshotRoutePlan plan_document_export_snapshot_route_for_current_platform(
|
||||
DocumentExportExecutionKind kind,
|
||||
DocumentCanvasSaveSnapshotReport report,
|
||||
std::string_view target_path = {}) noexcept
|
||||
{
|
||||
return plan_document_export_snapshot_route_for_target(
|
||||
kind,
|
||||
report,
|
||||
target_path,
|
||||
document_export_snapshot_platform_supported());
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentExportCollectionTargetPlan plan_document_export_collection_target(
|
||||
DocumentExportCollectionKind kind,
|
||||
bool use_work_directory_collection) noexcept
|
||||
{
|
||||
return {
|
||||
kind,
|
||||
use_work_directory_collection
|
||||
? DocumentExportCollectionDestination::work_directory_collection
|
||||
: DocumentExportCollectionDestination::picked_directory_stem,
|
||||
document_export_collection_suffix(kind),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_document_export_failure_dialog(
|
||||
DocumentExportSuccessKind kind,
|
||||
std::string_view status_message)
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
document_export_failure_dialog_title(kind),
|
||||
status_message,
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_document_export_license_disabled_dialog()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"License",
|
||||
"This function is disabled in demo mode.",
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline DocumentExportSuccessDialogPlan plan_document_export_success_dialog(
|
||||
DocumentExportSuccessKind kind,
|
||||
DocumentExportSuccessDestination destination,
|
||||
std::string_view detail = {})
|
||||
{
|
||||
DocumentExportSuccessDialogPlan plan;
|
||||
plan.kind = kind;
|
||||
plan.destination = destination;
|
||||
if (destination == DocumentExportSuccessDestination::suppressed) {
|
||||
return plan;
|
||||
}
|
||||
|
||||
std::string message;
|
||||
switch (kind) {
|
||||
case DocumentExportSuccessKind::equirectangular:
|
||||
plan.dialog.title = "Export Equirectangular";
|
||||
switch (destination) {
|
||||
case DocumentExportSuccessDestination::photos:
|
||||
message = "Image exported to Photos";
|
||||
break;
|
||||
case DocumentExportSuccessDestination::pictures_panopainter:
|
||||
message = "Image exported to Pictures/PanoPainter folder";
|
||||
break;
|
||||
case DocumentExportSuccessDestination::work_directory:
|
||||
message = "Image exported to ";
|
||||
message += detail;
|
||||
break;
|
||||
case DocumentExportSuccessDestination::suppressed:
|
||||
case DocumentExportSuccessDestination::files_panopainter:
|
||||
case DocumentExportSuccessDestination::path:
|
||||
case DocumentExportSuccessDestination::generic_success:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DocumentExportSuccessKind::layers:
|
||||
case DocumentExportSuccessKind::animation_frames:
|
||||
plan.dialog.title = "Export Layers";
|
||||
if (destination == DocumentExportSuccessDestination::files_panopainter) {
|
||||
message = "Image layers exported to Files/PanoPainter";
|
||||
} else if (destination == DocumentExportSuccessDestination::path) {
|
||||
message = "Layers exported to: ";
|
||||
message += detail;
|
||||
}
|
||||
break;
|
||||
case DocumentExportSuccessKind::depth:
|
||||
plan.dialog.title = "Export 3D View + Depth";
|
||||
if (destination == DocumentExportSuccessDestination::files_panopainter) {
|
||||
message = "Image and depth exported to Files/PanoPainter";
|
||||
} else if (destination == DocumentExportSuccessDestination::pictures_panopainter) {
|
||||
message = "Image and depth exported to Pictures/PanoPainter folder";
|
||||
} else if (destination == DocumentExportSuccessDestination::work_directory) {
|
||||
message = "Image and depth exported to ";
|
||||
message += detail;
|
||||
}
|
||||
break;
|
||||
case DocumentExportSuccessKind::cube_faces:
|
||||
plan.dialog.title = "Export Cube Faces";
|
||||
if (destination == DocumentExportSuccessDestination::files_panopainter) {
|
||||
message = "Image and depth exported to Files/PanoPainter";
|
||||
} else if (destination == DocumentExportSuccessDestination::pictures_panopainter) {
|
||||
message = "Image and depth exported to Pictures/PanoPainter folder";
|
||||
} else if (destination == DocumentExportSuccessDestination::work_directory) {
|
||||
message = "Image and depth exported to ";
|
||||
message += detail;
|
||||
}
|
||||
break;
|
||||
case DocumentExportSuccessKind::animation_mp4:
|
||||
plan.dialog.title = "Export Animation";
|
||||
if (destination == DocumentExportSuccessDestination::path) {
|
||||
message = "Animation exported to: ";
|
||||
message += detail;
|
||||
} else if (destination == DocumentExportSuccessDestination::generic_success) {
|
||||
message = "Animation exported successfully.";
|
||||
}
|
||||
break;
|
||||
case DocumentExportSuccessKind::timelapse:
|
||||
plan.dialog.title = "Export Timelapse";
|
||||
if (destination == DocumentExportSuccessDestination::path) {
|
||||
message = "Timelapse exported to: ";
|
||||
message += detail;
|
||||
} else if (destination == DocumentExportSuccessDestination::generic_success) {
|
||||
message = "Timelapse exported successfully.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!plan.dialog.title.empty() && !message.empty()) {
|
||||
plan.dialog.message = std::move(message);
|
||||
plan.show_dialog = true;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name,
|
||||
@@ -225,6 +759,114 @@ public:
|
||||
return pp::foundation::Result<DocumentExportStemTarget>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr std::array<std::string_view, pp::document::cube_face_count>
|
||||
document_cube_face_export_names() noexcept
|
||||
{
|
||||
return {
|
||||
"front",
|
||||
"right",
|
||||
"back",
|
||||
"left",
|
||||
"top",
|
||||
"bottom",
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCubeFaceExportTarget> make_document_cube_face_export_target(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name)
|
||||
{
|
||||
if (work_directory.empty()) {
|
||||
return pp::foundation::Result<DocumentCubeFaceExportTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("work directory must not be empty"));
|
||||
}
|
||||
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentCubeFaceExportTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
DocumentCubeFaceExportTarget target;
|
||||
const auto face_names = document_cube_face_export_names();
|
||||
target.face_count = face_names.size();
|
||||
for (std::size_t face_index = 0; face_index < face_names.size(); ++face_index) {
|
||||
auto& face = target.faces[face_index];
|
||||
face.face_name = face_names[face_index];
|
||||
face.path.reserve(work_directory.size() + document_name.size() + face_names[face_index].size() + 6);
|
||||
face.path += work_directory;
|
||||
face.path += "/";
|
||||
face.path += document_name;
|
||||
face.path += "-";
|
||||
face.path += face_names[face_index];
|
||||
face.path += ".png";
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentCubeFaceExportTarget>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentDepthExportTarget> make_document_depth_export_target(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name)
|
||||
{
|
||||
if (work_directory.empty()) {
|
||||
return pp::foundation::Result<DocumentDepthExportTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("work directory must not be empty"));
|
||||
}
|
||||
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentDepthExportTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
DocumentDepthExportTarget target;
|
||||
target.image_path.reserve(work_directory.size() + document_name.size() + 5U);
|
||||
target.image_path += work_directory;
|
||||
target.image_path += "/";
|
||||
target.image_path += document_name;
|
||||
target.image_path += ".png";
|
||||
target.depth_path.reserve(work_directory.size() + document_name.size() + 11U);
|
||||
target.depth_path += work_directory;
|
||||
target.depth_path += "/";
|
||||
target.depth_path += document_name;
|
||||
target.depth_path += "_depth.png";
|
||||
return pp::foundation::Result<DocumentDepthExportTarget>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string document_export_two_digit_index(std::size_t index)
|
||||
{
|
||||
auto value = std::to_string(index);
|
||||
if (value.size() < 2U) {
|
||||
value.insert(value.begin(), '0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_document_layer_export_path_suffix(
|
||||
std::size_t layer_index,
|
||||
std::string_view layer_name)
|
||||
{
|
||||
std::string suffix;
|
||||
const auto index = document_export_two_digit_index(layer_index);
|
||||
suffix.reserve(10U + index.size() + layer_name.size());
|
||||
suffix += "-layer";
|
||||
suffix += index;
|
||||
suffix += "-";
|
||||
suffix += layer_name;
|
||||
suffix += ".png";
|
||||
return suffix;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_document_animation_frame_export_path_suffix(std::size_t frame_index)
|
||||
{
|
||||
std::string suffix;
|
||||
const auto index = document_export_two_digit_index(frame_index);
|
||||
suffix.reserve(5U + index.size());
|
||||
suffix += "-";
|
||||
suffix += index;
|
||||
suffix += ".png";
|
||||
return suffix;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
|
||||
std::string_view document_name,
|
||||
std::string_view suffix)
|
||||
@@ -241,6 +883,226 @@ public:
|
||||
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_cube_face_export_write(
|
||||
const DocumentCubeFaceExportTarget& target,
|
||||
std::span<const DocumentCubeFaceExportPayload> face_payloads,
|
||||
DocumentCubeFaceExportWriteServices& services)
|
||||
{
|
||||
if (target.face_count != pp::document::cube_face_count) {
|
||||
return pp::foundation::Status::invalid_argument("cube face export target must contain all cube faces");
|
||||
}
|
||||
|
||||
if (face_payloads.size() != target.face_count) {
|
||||
return pp::foundation::Status::invalid_argument("cube face export payload count must match target face count");
|
||||
}
|
||||
|
||||
for (std::size_t face_index = 0; face_index < target.face_count; ++face_index) {
|
||||
const auto& face = target.faces[face_index];
|
||||
if (face.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("cube face export path must not be empty");
|
||||
}
|
||||
if (face_payloads[face_index].bytes.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("cube face export payload must not be empty");
|
||||
}
|
||||
|
||||
const auto write_status = services.write_binary_file(face.path, face_payloads[face_index].bytes);
|
||||
if (!write_status.ok()) {
|
||||
return write_status;
|
||||
}
|
||||
services.publish_exported_image(face.path);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_depth_export_write(
|
||||
const DocumentDepthExportTarget& target,
|
||||
DocumentDepthExportPayload payload,
|
||||
DocumentDepthExportWriteServices& services)
|
||||
{
|
||||
if (target.image_path.empty() || target.depth_path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("depth export target requires image and depth paths");
|
||||
}
|
||||
if (payload.image_bytes.empty() || payload.depth_bytes.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("depth export payload requires image and depth bytes");
|
||||
}
|
||||
|
||||
const auto image_status = services.write_binary_file(target.image_path, payload.image_bytes);
|
||||
if (!image_status.ok()) {
|
||||
return image_status;
|
||||
}
|
||||
services.publish_exported_image(target.image_path);
|
||||
|
||||
const auto depth_status = services.write_binary_file(target.depth_path, payload.depth_bytes);
|
||||
if (!depth_status.ok()) {
|
||||
return depth_status;
|
||||
}
|
||||
services.publish_exported_image(target.depth_path);
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_file_write(
|
||||
const DocumentExportFileTarget& target,
|
||||
DocumentExportFilePayload payload,
|
||||
DocumentExportFileWriteServices& services)
|
||||
{
|
||||
if (target.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export file target requires a path");
|
||||
}
|
||||
if (payload.bytes.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export file payload must not be empty");
|
||||
}
|
||||
|
||||
const auto write_status = services.write_binary_file(target.path, payload.bytes);
|
||||
if (!write_status.ok()) {
|
||||
return write_status;
|
||||
}
|
||||
services.publish_exported_image(target.path);
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_collection_write(
|
||||
const DocumentExportCollectionTarget& target,
|
||||
std::span<const DocumentExportCollectionPngPayload> payloads,
|
||||
DocumentExportCollectionWriteServices& services)
|
||||
{
|
||||
if (target.stem_path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export collection target requires a stem path");
|
||||
}
|
||||
|
||||
if (payloads.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export collection payloads must not be empty");
|
||||
}
|
||||
|
||||
for (const auto& payload : payloads) {
|
||||
if (payload.path_suffix.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export collection payload suffix must not be empty");
|
||||
}
|
||||
if (payload.bytes.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export collection payload must not be empty");
|
||||
}
|
||||
|
||||
std::string path;
|
||||
path.reserve(target.stem_path.size() + payload.path_suffix.size());
|
||||
path += target.stem_path;
|
||||
path += payload.path_suffix;
|
||||
const auto write_status = services.write_binary_file(path, payload.bytes);
|
||||
if (!write_status.ok()) {
|
||||
return write_status;
|
||||
}
|
||||
services.publish_exported_image(path);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_file(
|
||||
const DocumentExportFileTarget& target,
|
||||
DocumentExportServices& services)
|
||||
{
|
||||
if (target.path.empty() || target.suggested_name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export file target requires a path and suggested name");
|
||||
}
|
||||
|
||||
services.export_equirectangular(target);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_stem(
|
||||
DocumentExportCollectionKind kind,
|
||||
const DocumentExportStemTarget& target,
|
||||
DocumentExportServices& services)
|
||||
{
|
||||
if (target.stem_path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export stem target requires a stem path");
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case DocumentExportCollectionKind::layers:
|
||||
services.export_layers_to_stem(target);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportCollectionKind::animation_frames:
|
||||
services.export_animation_frames_to_stem(target);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document export collection kind");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_collection(
|
||||
DocumentExportCollectionKind kind,
|
||||
const DocumentExportCollectionTarget& target,
|
||||
DocumentExportServices& services)
|
||||
{
|
||||
if (target.directory.empty() || target.stem_path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("export collection target requires a directory and stem path");
|
||||
}
|
||||
|
||||
if (!services.create_directory(target.directory)) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case DocumentExportCollectionKind::layers:
|
||||
services.export_layers_to_collection(target);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentExportCollectionKind::animation_frames:
|
||||
services.export_animation_frames_to_collection(target);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document export collection kind");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_depth(
|
||||
std::string_view document_name,
|
||||
DocumentExportServices& services)
|
||||
{
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document name must not be empty");
|
||||
}
|
||||
|
||||
services.export_depth(document_name);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_cube_faces(
|
||||
std::string_view document_name,
|
||||
DocumentExportServices& services)
|
||||
{
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document name must not be empty");
|
||||
}
|
||||
|
||||
services.export_cube_faces(document_name);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_video_export(
|
||||
DocumentVideoExportKind kind,
|
||||
std::string_view path,
|
||||
DocumentVideoExportServices& services)
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("video export path must not be empty");
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case DocumentVideoExportKind::animation_mp4:
|
||||
services.export_animation_mp4(path);
|
||||
services.show_animation_export_success(path);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentVideoExportKind::timelapse:
|
||||
services.export_timelapse_mp4(path);
|
||||
services.show_timelapse_export_success(path);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document video export kind");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan(
|
||||
const DocumentExportMenuPlan& plan,
|
||||
DocumentExportMenuServices& services)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
@@ -78,6 +79,39 @@ struct DocumentLayerMenuPlan {
|
||||
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;
|
||||
@@ -88,6 +122,75 @@ public:
|
||||
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
|
||||
@@ -118,6 +221,60 @@ public:
|
||||
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)
|
||||
@@ -420,6 +577,161 @@ public:
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
@@ -26,6 +30,27 @@ struct RecordingExportPlan {
|
||||
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
|
||||
@@ -60,4 +85,80 @@ struct RecordingExportPlan {
|
||||
};
|
||||
}
|
||||
|
||||
[[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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#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>
|
||||
@@ -55,6 +57,49 @@ enum class DocumentOpenPlanAction {
|
||||
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;
|
||||
@@ -71,12 +116,84 @@ struct DocumentFileSavePlan {
|
||||
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
|
||||
@@ -102,6 +219,41 @@ struct NewDocumentPlan {
|
||||
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
|
||||
@@ -115,6 +267,24 @@ struct NewDocumentPlan {
|
||||
: 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,
|
||||
@@ -146,6 +316,27 @@ struct NewDocumentPlan {
|
||||
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
|
||||
@@ -159,6 +350,24 @@ struct NewDocumentPlan {
|
||||
: 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)
|
||||
@@ -204,6 +413,22 @@ template <typename ExistsPredicate>
|
||||
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 };
|
||||
@@ -242,6 +467,22 @@ template <typename ExistsPredicate>
|
||||
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('.');
|
||||
@@ -320,4 +561,16 @@ template <typename ExistsPredicate>
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,17 @@ struct GridUiPlan {
|
||||
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) {
|
||||
@@ -142,4 +153,57 @@ struct GridUiPlan {
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_canvas.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
@@ -59,6 +60,14 @@ public:
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class QuickUiSlotKind {
|
||||
@@ -35,6 +37,27 @@ struct QuickUiPlan {
|
||||
bool mutates_quick_state = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewInput {
|
||||
bool ui_rtl = false;
|
||||
float slider_x = 0.0F;
|
||||
float slider_y = 0.0F;
|
||||
float slider_height = 0.0F;
|
||||
float zoom = 1.0F;
|
||||
bool has_pen_mode = false;
|
||||
bool has_line_mode = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewPlan {
|
||||
float cursor_x = 0.0F;
|
||||
float cursor_y = 0.0F;
|
||||
bool updates_pen_mode = false;
|
||||
bool updates_line_mode = false;
|
||||
bool draws_tip = false;
|
||||
bool disables_pen_outline = false;
|
||||
bool redraws_brush_preview = false;
|
||||
bool invokes_change_callback = false;
|
||||
};
|
||||
|
||||
class QuickUiServices {
|
||||
public:
|
||||
virtual ~QuickUiServices() = default;
|
||||
@@ -45,6 +68,15 @@ public:
|
||||
virtual void reset_state(bool fire_event) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slider_float(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("quick slider preview value must be finite");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
|
||||
{
|
||||
if (slot_count <= 0) {
|
||||
@@ -68,6 +100,47 @@ public:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickSliderPreviewPlan> plan_quick_slider_preview(
|
||||
const QuickSliderPreviewInput& input)
|
||||
{
|
||||
const auto x_status = validate_quick_slider_float(input.slider_x);
|
||||
if (!x_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(x_status);
|
||||
}
|
||||
const auto y_status = validate_quick_slider_float(input.slider_y);
|
||||
if (!y_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(y_status);
|
||||
}
|
||||
const auto height_status = validate_quick_slider_float(input.slider_height);
|
||||
if (!height_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(height_status);
|
||||
}
|
||||
if (input.slider_height < 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview height must not be negative"));
|
||||
}
|
||||
const auto zoom_status = validate_quick_slider_float(input.zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(zoom_status);
|
||||
}
|
||||
if (input.zoom <= 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview zoom must be positive"));
|
||||
}
|
||||
|
||||
const float offset = input.ui_rtl ? -100.0F : 100.0F;
|
||||
QuickSliderPreviewPlan plan;
|
||||
plan.cursor_x = (input.slider_x + offset) * input.zoom;
|
||||
plan.cursor_y = (input.slider_y + input.slider_height * 0.5F) * input.zoom;
|
||||
plan.updates_pen_mode = input.has_pen_mode;
|
||||
plan.updates_line_mode = input.has_line_mode;
|
||||
plan.draws_tip = input.has_pen_mode || input.has_line_mode;
|
||||
plan.disables_pen_outline = input.has_pen_mode;
|
||||
plan.redraws_brush_preview = true;
|
||||
plan.invokes_change_callback = true;
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
|
||||
QuickUiSlotKind slot_kind,
|
||||
int current_index,
|
||||
|
||||
@@ -1,687 +1,88 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "action.h"
|
||||
#include "app_core/document_layer.h"
|
||||
#include "app_core/document_resize.h"
|
||||
#include "app_core/document_export.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "settings.h"
|
||||
#include "node_dialog_open.h"
|
||||
#include "node_dialog_browse.h"
|
||||
#include "node_dialog_resize.h"
|
||||
#include "node_dialog_cloud.h"
|
||||
#include "node_about.h"
|
||||
#include "node_changelog.h"
|
||||
#include "node_usermanual.h"
|
||||
#include "node_dialog_export_ppbr.h"
|
||||
#include "node_remote_page.h"
|
||||
#include "node_shorcuts.h"
|
||||
|
||||
#include <codec_api.h>
|
||||
#define MP4V2_NO_STDINT_DEFS
|
||||
#include <mp4v2/mp4v2.h>
|
||||
#include "legacy_app_dialog_services.h"
|
||||
#include "legacy_document_layer_services.h"
|
||||
|
||||
#ifdef __QUEST__
|
||||
#include "oculus_vr.h"
|
||||
#elif __WEB__
|
||||
void webgl_pick_file(std::function<void(std::string)> callback);
|
||||
void webgl_sync();
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
|
||||
{
|
||||
const auto decision = pp::app::plan_document_export_start(
|
||||
requires_license,
|
||||
!requires_license || app.check_license(),
|
||||
app.canvas != nullptr);
|
||||
|
||||
switch (decision) {
|
||||
case pp::app::DocumentExportStartDecision::start_now:
|
||||
return true;
|
||||
case pp::app::DocumentExportStartDecision::show_license_disabled:
|
||||
app.message_box("License", "This function is disabled in demo mode.");
|
||||
return false;
|
||||
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace pp::panopainter {
|
||||
void open_document_export_dialog(App& app, std::string ext);
|
||||
void open_document_export_layers_dialog(App& app);
|
||||
void open_document_export_anim_frames_dialog(App& app);
|
||||
void open_document_export_depth_dialog(App& app);
|
||||
void open_document_export_cube_faces_dialog(App& app);
|
||||
void open_ppbr_export_dialog(App& app);
|
||||
void open_document_timelapse_export_dialog(App& app);
|
||||
void open_document_export_mp4_dialog(App& app);
|
||||
void open_usermanual_dialog(App& app);
|
||||
void open_changelog_dialog(App& app);
|
||||
void open_about_dialog(App& app);
|
||||
void open_whatsnew_dialog(App& app, bool force_show);
|
||||
void open_shortcuts_dialog(App& app);
|
||||
void open_legacy_document_layer_rename_dialog(App& app);
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
|
||||
{
|
||||
auto pb = std::make_shared<NodeProgressBar>();
|
||||
pb->set_manager(&layout);
|
||||
pb->init();
|
||||
pb->create();
|
||||
pb->loaded();
|
||||
pb->m_progress->SetWidthP(0);
|
||||
pb->m_title->set_text(title.c_str());
|
||||
pb->m_total = total;
|
||||
pb->m_count = 0;
|
||||
layout[main_id]->add_child(pb);
|
||||
return pb;
|
||||
return pp::panopainter::show_legacy_app_progress_dialog(*this, title, total);
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeMessageBox> App::message_box(const std::string &title, const std::string& text, bool cancel_button)
|
||||
{
|
||||
auto m = std::make_shared<NodeMessageBox>();
|
||||
m->set_manager(&layout);
|
||||
m->init();
|
||||
m->create();
|
||||
m->loaded();
|
||||
m->m_title->set_text(title.c_str());
|
||||
m->m_message->set_text(text.c_str());
|
||||
m->btn_ok->m_text->set_text("Ok");
|
||||
if (!cancel_button)
|
||||
m->btn_cancel->destroy();
|
||||
layout[main_id]->add_child(m);
|
||||
return m;
|
||||
return pp::panopainter::show_legacy_app_message_dialog(*this, title, text, cancel_button);
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeInputBox> App::input_box(const std::string& title,
|
||||
const std::string& field_name, const std::string& ok_caption /*= "Ok"*/)
|
||||
{
|
||||
auto m = std::make_shared<NodeInputBox>();
|
||||
m->set_manager(&layout);
|
||||
m->init();
|
||||
m->create();
|
||||
m->loaded();
|
||||
m->m_title->set_text(title.c_str());
|
||||
m->m_field_name->set_text(field_name.c_str());
|
||||
m->btn_ok->m_text->set_text(ok_caption.c_str());
|
||||
layout[main_id]->add_child(m);
|
||||
return m;
|
||||
return pp::panopainter::show_legacy_app_input_dialog(*this, title, field_name, ok_caption);
|
||||
}
|
||||
|
||||
void App::dialog_usermanual()
|
||||
{
|
||||
auto dialog = std::make_shared<NodeUserManual>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
pp::panopainter::open_usermanual_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_changelog()
|
||||
{
|
||||
auto dialog = std::make_shared<NodeChangelog>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
pp::panopainter::open_changelog_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_about()
|
||||
{
|
||||
auto dialog = std::make_shared<NodeAbout>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
}
|
||||
|
||||
void App::continue_document_workflow_after_optional_save(std::function<void()> action)
|
||||
{
|
||||
const bool has_canvas = canvas != nullptr;
|
||||
const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved;
|
||||
const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes);
|
||||
switch (decision) {
|
||||
case pp::app::DocumentWorkflowDecision::unavailable:
|
||||
return;
|
||||
case pp::app::DocumentWorkflowDecision::continue_now:
|
||||
action();
|
||||
return;
|
||||
case pp::app::DocumentWorkflowDecision::prompt_save_before_continue:
|
||||
break;
|
||||
}
|
||||
|
||||
auto m = layout[main_id]->add_child<NodeMessageBox>();
|
||||
m->m_title->set_text("Unsaved document");
|
||||
m->m_message->set_text("Would you like to save this document before closing?");
|
||||
m->btn_ok->m_text->set_text("Yes");
|
||||
m->btn_cancel->m_text->set_text("No");
|
||||
m->btn_ok->on_click = [this, m, action](Node*) {
|
||||
Canvas::I->project_save([this, m, action](bool success) {
|
||||
if (success)
|
||||
action();
|
||||
else
|
||||
message_box("Saving Error", "There was a problem saving the document");
|
||||
});
|
||||
m->destroy();
|
||||
};
|
||||
m->btn_cancel->on_click = [m, action](Node*) {
|
||||
action();
|
||||
m->destroy();
|
||||
};
|
||||
}
|
||||
|
||||
void App::dialog_newdoc()
|
||||
{
|
||||
auto show_dialog = [this] {
|
||||
auto dialog = std::make_shared<NodeDialogNewDoc>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
dialog->input->set_text("name");
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
App::I->showKeyboard();
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
std::string name = dialog->input->m_text;
|
||||
const auto plan = pp::app::plan_new_document(
|
||||
work_path,
|
||||
name,
|
||||
dialog->m_resolution->m_current_index,
|
||||
[](const std::string& path) {
|
||||
return Asset::exist(path);
|
||||
});
|
||||
if (!plan)
|
||||
{
|
||||
const bool missing_name =
|
||||
plan.status().code == pp::foundation::StatusCode::invalid_argument;
|
||||
message_box(
|
||||
"Warning",
|
||||
missing_name ? "You need to specify a name to file." : plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
auto action = [this, dialog, plan = plan.value()] {
|
||||
doc_name = plan.target.name;
|
||||
doc_path = plan.target.path;
|
||||
doc_filename = plan.target.name + ".ppi";
|
||||
doc_dir = plan.target.directory;
|
||||
|
||||
layers->clear();
|
||||
canvas->m_canvas->m_layers.clear();
|
||||
canvas->m_canvas->resize(plan.resolution, plan.resolution);
|
||||
canvas->reset_camera();
|
||||
ActionManager::clear();
|
||||
|
||||
layers->add_layer("Default", false, true);
|
||||
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
canvas->m_canvas->m_newdoc = false;
|
||||
title_update();
|
||||
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
|
||||
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||
{
|
||||
// ask confirm is file already exist
|
||||
auto msgbox = new NodeMessageBox();
|
||||
msgbox->set_manager(&layout);
|
||||
msgbox->init();
|
||||
msgbox->m_title->set_text("Warning");
|
||||
msgbox->m_message->set_text("A document with this name already exists, continue?");
|
||||
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
|
||||
action();
|
||||
msgbox->destroy();
|
||||
};
|
||||
layout[main_id]->add_child(msgbox);
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
};
|
||||
dialog->btn_cancel->on_click = [this, dialog](Node*)
|
||||
{
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
};
|
||||
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
void App::dialog_open()
|
||||
{
|
||||
auto show_dialog = [this] {
|
||||
// load thumbnail test
|
||||
auto dialog = std::make_shared<NodeDialogOpen>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
// canvas->reset_camera();
|
||||
// layers->clear();
|
||||
// doc_name = dialog->selected_name;
|
||||
// canvas->m_canvas->project_open(dialog->selected_path, [this](bool success) {
|
||||
// // on complete
|
||||
// async_start();
|
||||
// title_update();
|
||||
// for (auto& i : canvas->m_canvas->m_order)
|
||||
// layers->add_layer(canvas->m_canvas->m_layers[i]->m_name.c_str());
|
||||
// async_end();
|
||||
// });
|
||||
// dialog->destroy();
|
||||
// ActionManager::clear();
|
||||
};
|
||||
};
|
||||
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
void App::dialog_browse()
|
||||
{
|
||||
auto show_dialog = [this] {
|
||||
// load thumbnail test
|
||||
auto dialog = std::make_shared<NodeDialogBrowse>();
|
||||
dialog->set_manager(&layout);
|
||||
#ifdef __IOS__
|
||||
dialog->search_paths = {work_path, data_path + "/Inbox"};
|
||||
#else
|
||||
dialog->search_paths = {work_path};
|
||||
#endif
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
if (dialog->is_selected())
|
||||
{
|
||||
open_document(dialog->selected_path);
|
||||
dialog->destroy();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
void App::dialog_save_ver()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto target = pp::app::find_next_document_version_target(
|
||||
doc_dir,
|
||||
doc_name,
|
||||
[](const std::string& path) {
|
||||
return Asset::exist(path);
|
||||
});
|
||||
if (!target) {
|
||||
message_box("Saving Error", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
doc_name = target.value().name;
|
||||
doc_path = target.value().path;
|
||||
canvas->m_canvas->m_unsaved = true;
|
||||
title_update();
|
||||
canvas->m_canvas->project_save(doc_path);
|
||||
}
|
||||
|
||||
void App::save_document(pp::app::DocumentSaveIntent intent)
|
||||
{
|
||||
const auto decision = pp::app::plan_document_save(
|
||||
Canvas::I->m_newdoc,
|
||||
Canvas::I->m_unsaved,
|
||||
intent);
|
||||
switch (decision) {
|
||||
case pp::app::DocumentSaveDecision::show_save_dialog:
|
||||
dialog_save();
|
||||
break;
|
||||
case pp::app::DocumentSaveDecision::save_existing:
|
||||
Canvas::I->project_save();
|
||||
break;
|
||||
case pp::app::DocumentSaveDecision::save_version:
|
||||
dialog_save_ver();
|
||||
break;
|
||||
case pp::app::DocumentSaveDecision::no_op:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void App::dialog_save()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (canvas)
|
||||
{
|
||||
auto dialog = std::make_shared<NodeDialogSave>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
dialog->input->set_text(doc_name);
|
||||
|
||||
App::I->showKeyboard();
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
std::string name = dialog->input->m_text;
|
||||
const auto plan = pp::app::plan_document_file_save(
|
||||
work_path,
|
||||
name,
|
||||
[](const std::string& path) {
|
||||
return Asset::exist(path);
|
||||
});
|
||||
if (!plan)
|
||||
{
|
||||
message_box("Warning", "You need to specify a name to file.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto action = [this, dialog, plan = plan.value()] {
|
||||
canvas->m_canvas->project_save(plan.target.path);
|
||||
doc_name = plan.target.name;
|
||||
doc_path = plan.target.path;
|
||||
doc_dir = plan.target.directory;
|
||||
title_update();
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
|
||||
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
|
||||
{
|
||||
// ask confirm is file already exist
|
||||
auto msgbox = new NodeMessageBox();
|
||||
msgbox->set_manager(&layout);
|
||||
msgbox->init();
|
||||
msgbox->m_title->set_text("Warning");
|
||||
msgbox->m_message->set_text(("Are you sure you want to overwrite " + plan.value().target.name + "?").c_str());
|
||||
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
|
||||
action();
|
||||
msgbox->destroy();
|
||||
};
|
||||
layout[main_id]->add_child(msgbox);
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
};
|
||||
dialog->btn_cancel->on_click = [this, dialog](Node*)
|
||||
{
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
}
|
||||
pp::panopainter::open_about_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_export(std::string ext)
|
||||
{
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
// TODO: use picker
|
||||
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
|
||||
if (!target) {
|
||||
message_box("Export Equirectangular", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
|
||||
#if defined(__IOS__)
|
||||
message_box("Export Equirectangular", "Image exported to Photos");
|
||||
#elif defined(__OSX__)
|
||||
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
|
||||
#elif defined(_WIN32)
|
||||
message_box("Export Equirectangular", "Image exported to " + work_path);
|
||||
#elif defined(__QUEST__)
|
||||
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
|
||||
#elif __WEB__
|
||||
ui_task([=]{
|
||||
save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
|
||||
});
|
||||
#endif
|
||||
});
|
||||
pp::panopainter::open_document_export_dialog(*this, ext);
|
||||
}
|
||||
|
||||
void App::dialog_export_layers()
|
||||
{
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
#if defined(__IOS__)
|
||||
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
|
||||
if (!target) {
|
||||
message_box("Export Layers", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Asset::create_dir(target.value().directory))
|
||||
{
|
||||
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
|
||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||
});
|
||||
}
|
||||
#else
|
||||
pick_dir([this](std::string path) {
|
||||
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
|
||||
if (!target) {
|
||||
message_box("Export Layers", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
|
||||
message_box("Export Layers", "Layers exported to: " + target.stem_path);
|
||||
});
|
||||
});
|
||||
#endif
|
||||
pp::panopainter::open_document_export_layers_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_export_anim_frames()
|
||||
{
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
#if defined(__IOS__)
|
||||
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
|
||||
if (!target) {
|
||||
message_box("Export Layers", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Asset::create_dir(target.value().directory))
|
||||
{
|
||||
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
|
||||
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||
});
|
||||
}
|
||||
#else
|
||||
pick_dir([this](std::string path) {
|
||||
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
|
||||
if (!target) {
|
||||
message_box("Export Layers", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] {
|
||||
message_box("Export Layers", "Layers exported to: " + target.stem_path);
|
||||
});
|
||||
});
|
||||
#endif
|
||||
pp::panopainter::open_document_export_anim_frames_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_export_depth()
|
||||
{
|
||||
if (!can_start_document_export(*this, true))
|
||||
return;
|
||||
|
||||
// TODO: use picker
|
||||
canvas->m_canvas->export_depth(doc_name, [this] {
|
||||
#if defined(__IOS__)
|
||||
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
|
||||
#elif defined(__OSX__)
|
||||
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
|
||||
#elif defined(_WIN32)
|
||||
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
|
||||
#endif
|
||||
});
|
||||
}
|
||||
|
||||
void App::dialog_resize()
|
||||
{
|
||||
class LegacyDocumentResizeServices final : public pp::app::DocumentResizeServices {
|
||||
public:
|
||||
explicit LegacyDocumentResizeServices(App& app) noexcept
|
||||
: app_(app)
|
||||
{
|
||||
}
|
||||
|
||||
void resize_document(int width, int height) override
|
||||
{
|
||||
if (app_.canvas)
|
||||
app_.canvas->m_canvas->resize(width, height);
|
||||
}
|
||||
|
||||
void update_title() override
|
||||
{
|
||||
app_.title_update();
|
||||
}
|
||||
|
||||
void clear_history() override
|
||||
{
|
||||
ActionManager::clear();
|
||||
}
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
};
|
||||
|
||||
auto dialog = std::make_shared<NodeDialogResize>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this,dialog](Node*)
|
||||
{
|
||||
const auto plan = pp::app::plan_document_resize(
|
||||
dialog->combo ? dialog->combo->m_current_index : 0);
|
||||
if (!plan)
|
||||
{
|
||||
dialog->destroy();
|
||||
return;
|
||||
}
|
||||
LegacyDocumentResizeServices services(*this);
|
||||
const auto status = pp::app::execute_document_resize_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Document resize failed: %s", status.message);
|
||||
dialog->destroy();
|
||||
};
|
||||
pp::panopainter::open_document_export_depth_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_export_cube_faces()
|
||||
{
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
canvas->m_canvas->export_cube_faces(doc_name, [this] {
|
||||
#if defined(__IOS__)
|
||||
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
|
||||
#elif defined(__OSX__)
|
||||
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
|
||||
#elif defined(_WIN32)
|
||||
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
|
||||
#endif
|
||||
});
|
||||
pp::panopainter::open_document_export_cube_faces_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_layer_rename()
|
||||
{
|
||||
auto dialog = std::make_shared<NodeDialogLayerRename>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
dialog->input->set_text(layers->m_current_layer->m_label_text);
|
||||
|
||||
App::I->showKeyboard();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this,dialog](Node*)
|
||||
{
|
||||
const auto old_name = layers->m_current_layer->m_label_text;
|
||||
const auto plan = pp::app::plan_document_layer_rename(old_name, dialog->get_name());
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().action == pp::app::DocumentLayerRenameAction::no_op_same_name)
|
||||
{
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
return;
|
||||
}
|
||||
|
||||
struct ActionLayerRename : public Action
|
||||
{
|
||||
std::string m_old_name;
|
||||
std::string m_new_name;
|
||||
bool m_unsaved;
|
||||
Layer* m_layer;
|
||||
std::shared_ptr<NodeLayer> m_layer_node;
|
||||
ActionLayerRename(std::string old_name, std::string new_name, std::shared_ptr<NodeLayer> layer_node, Layer* layer) :
|
||||
m_old_name(old_name), m_new_name(new_name), m_layer_node(layer_node), m_layer(layer) { }
|
||||
virtual void run() override { }
|
||||
virtual size_t memory() override { return 0; }
|
||||
virtual void undo() override
|
||||
{
|
||||
m_layer_node->set_name(m_old_name.c_str());
|
||||
m_layer->m_name = m_old_name;
|
||||
}
|
||||
virtual Action* get_redo() override
|
||||
{
|
||||
return new ActionLayerRename(m_new_name, m_old_name, m_layer_node, m_layer);
|
||||
}
|
||||
};
|
||||
auto layer_node = std::static_pointer_cast<NodeLayer>(layers->m_current_layer->shared_from_this());
|
||||
auto* layer = canvas->m_canvas->m_layers[canvas->m_canvas->m_current_layer_idx].get();
|
||||
ActionManager::add(new ActionLayerRename(
|
||||
plan.value().old_name,
|
||||
plan.value().new_name,
|
||||
layer_node,
|
||||
layer));
|
||||
layer_node->set_name(plan.value().new_name.c_str());
|
||||
layer->m_name = plan.value().new_name;
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
dialog->btn_cancel->on_click = [this, dialog](Node*)
|
||||
{
|
||||
dialog->destroy();
|
||||
App::I->hideKeyboard();
|
||||
};
|
||||
pp::panopainter::open_legacy_document_layer_rename_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_preset_download()
|
||||
@@ -691,137 +92,25 @@ void App::dialog_preset_download()
|
||||
|
||||
void App::dialog_ppbr_export()
|
||||
{
|
||||
auto root = layout[main_id];
|
||||
auto dialog = root->add_child_ref<NodeDialogExportPPBR>();
|
||||
dialog->btn_ok->on_click = [this, dialog] (Node*) {
|
||||
NodePanelBrushPreset::PPBRInfo info;
|
||||
info.author = dialog->txt_author->m_text;
|
||||
info.url = dialog->txt_url->m_text;
|
||||
info.email = dialog->txt_email->m_text;
|
||||
info.descr = dialog->txt_descr->m_text;
|
||||
info.header_image = dialog->m_header_image;
|
||||
info.dest_path = dialog->m_dest_path;
|
||||
if (dialog->export_check)
|
||||
info.export_data = dialog->export_check->checked;
|
||||
#if __IOS__ || __WEB__
|
||||
App::I->pick_file_save("ppbr", "exported-brushes",
|
||||
[this, dialog, info] (std::string path) {
|
||||
presets->export_ppbr(path, info);
|
||||
},
|
||||
[dialog] (const std::string& path, bool saved) {
|
||||
if (saved)
|
||||
dialog->destroy();
|
||||
}
|
||||
);
|
||||
#else
|
||||
App::I->pick_file_save({ "ppbr" }, [this, dialog, info] (std::string path) {
|
||||
std::thread([this, path, dialog, info] {
|
||||
BT_SetTerminate();
|
||||
presets->export_ppbr(path, info);
|
||||
dialog->destroy();
|
||||
App::I->message_box("Export PPBR", "Brushes exported to:\n" + path);
|
||||
}).detach();
|
||||
});
|
||||
#endif
|
||||
};
|
||||
pp::panopainter::open_ppbr_export_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_timelapse_export()
|
||||
{
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
#if __IOS__ || __WEB__
|
||||
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
|
||||
if (!target) {
|
||||
message_box("Export Timelapse", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
pick_file_save("mp4", target.value().name,
|
||||
[this](std::string path) {
|
||||
rec_export(path);
|
||||
},
|
||||
[this](const std::string& path, bool saved) {
|
||||
message_box("Export Timelapse", "Timelapse exported successfully.");
|
||||
}
|
||||
);
|
||||
#else
|
||||
pick_file_save({ "mp4" }, [this](std::string path) {
|
||||
std::thread([this, path] {
|
||||
BT_SetTerminate();
|
||||
rec_export(path);
|
||||
message_box("Export Timelapse", "Timelapse exported to: " + path);
|
||||
}).detach();
|
||||
});
|
||||
#endif
|
||||
pp::panopainter::open_document_timelapse_export_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_export_mp4()
|
||||
{
|
||||
if (!can_start_document_export(*this, false))
|
||||
return;
|
||||
|
||||
#if __IOS__ || __WEB__
|
||||
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
|
||||
if (!target) {
|
||||
message_box("Export Animation", target.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
pick_file_save("mp4", target.value().name,
|
||||
[this](std::string path) {
|
||||
export_anim_mp4(path);
|
||||
},
|
||||
[this](const std::string& path, bool saved) {
|
||||
message_box("Export Animation", "Animation exported successfully.");
|
||||
}
|
||||
);
|
||||
#else
|
||||
pick_file_save({ "mp4" }, [this](std::string path) {
|
||||
Canvas::I->export_anim_mp4(path, [this, path] {
|
||||
message_box("Export Animation", "Animation exported to: " + path);
|
||||
});
|
||||
});
|
||||
#endif
|
||||
pp::panopainter::open_document_export_mp4_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_whatsnew(bool force_show)
|
||||
{
|
||||
auto whatsnew = std::make_shared<NodeRemotePage>();
|
||||
whatsnew->m_manager = &layout;
|
||||
whatsnew->init();
|
||||
std::string url = fmt::format("https://panopainter.com/app-content/whatsnew/?version={}", g_version_build);
|
||||
whatsnew->load_url(url, [this, whatsnew, force_show](bool success) {
|
||||
if (success)
|
||||
{
|
||||
int last_id = Settings::value_or<Serializer::Integer>("whatsnew-id", 0);
|
||||
if (force_show || (whatsnew->m_page_id <= g_version_build && whatsnew->m_page_id > last_id))
|
||||
{
|
||||
whatsnew->set_title(fmt::format("What's new in version {}", g_version_number));
|
||||
if (!force_show)
|
||||
layout[main_id]->add_child(whatsnew);
|
||||
}
|
||||
}
|
||||
});
|
||||
whatsnew->add_button("Reload", 120, [this, whatsnew](Node*) {
|
||||
whatsnew->reload();
|
||||
});
|
||||
whatsnew->add_button("Read Later", 120, [this, whatsnew](Node*) {
|
||||
Settings::unset("whatsnew-id");
|
||||
Settings::save();
|
||||
whatsnew->destroy();
|
||||
});
|
||||
whatsnew->add_button("Close", 100, [this, whatsnew](Node*) {
|
||||
Settings::set<Serializer::Integer>("whatsnew-id", whatsnew->m_page_id);
|
||||
Settings::save();
|
||||
whatsnew->destroy();
|
||||
});
|
||||
if (force_show)
|
||||
layout[main_id]->add_child(whatsnew);
|
||||
pp::panopainter::open_whatsnew_dialog(*this, force_show);
|
||||
}
|
||||
|
||||
void App::dialog_shortcuts()
|
||||
{
|
||||
layout[main_id]->add_child<NodeShortcuts>();
|
||||
pp::panopainter::open_shortcuts_dialog(*this);
|
||||
}
|
||||
|
||||
51
src/app_dialogs_export.cpp
Normal file
51
src/app_dialogs_export.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "app.h"
|
||||
#include "legacy_brush_package_export_services.h"
|
||||
#include "legacy_document_export_services.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void open_document_export_dialog(App& app, std::string ext)
|
||||
{
|
||||
open_legacy_document_export_dialog(app, std::move(ext));
|
||||
}
|
||||
|
||||
void open_document_export_layers_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_export_layers_dialog(app);
|
||||
}
|
||||
|
||||
void open_document_export_anim_frames_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_export_anim_frames_dialog(app);
|
||||
}
|
||||
|
||||
void open_document_export_depth_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_export_depth_dialog(app);
|
||||
}
|
||||
|
||||
void open_document_export_cube_faces_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_export_cube_faces_dialog(app);
|
||||
}
|
||||
|
||||
void open_ppbr_export_dialog(App& app)
|
||||
{
|
||||
open_legacy_ppbr_export_dialog(app);
|
||||
}
|
||||
|
||||
void open_document_timelapse_export_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_timelapse_export_dialog(app);
|
||||
}
|
||||
|
||||
void open_document_export_mp4_dialog(App& app)
|
||||
{
|
||||
open_legacy_document_export_mp4_dialog(app);
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
51
src/app_dialogs_info_openers.cpp
Normal file
51
src/app_dialogs_info_openers.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_info_dialog_services.h"
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
namespace {
|
||||
|
||||
Node* get_legacy_info_dialog_overlay_anchor(App& app, const char* log_name)
|
||||
{
|
||||
auto* overlay_anchor = app.layout[app.main_id];
|
||||
if (!overlay_anchor) {
|
||||
LOG("%s dialog open failed: main layout anchor is missing", log_name);
|
||||
}
|
||||
return overlay_anchor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void open_usermanual_dialog(App& app)
|
||||
{
|
||||
if (auto* overlay_anchor = get_legacy_info_dialog_overlay_anchor(app, "User manual")) {
|
||||
pp::panopainter::open_usermanual_dialog(app, *overlay_anchor, "User manual");
|
||||
}
|
||||
}
|
||||
|
||||
void open_changelog_dialog(App& app)
|
||||
{
|
||||
if (auto* overlay_anchor = get_legacy_info_dialog_overlay_anchor(app, "Changelog")) {
|
||||
pp::panopainter::open_changelog_dialog(app, *overlay_anchor, "Changelog");
|
||||
}
|
||||
}
|
||||
|
||||
void open_about_dialog(App& app)
|
||||
{
|
||||
if (auto* overlay_anchor = get_legacy_info_dialog_overlay_anchor(app, "About")) {
|
||||
pp::panopainter::open_about_dialog(app, *overlay_anchor, "About");
|
||||
}
|
||||
}
|
||||
|
||||
void open_whatsnew_dialog(App& app, bool force_show)
|
||||
{
|
||||
pp::panopainter::open_legacy_whatsnew_dialog(app, force_show);
|
||||
}
|
||||
|
||||
void open_shortcuts_dialog(App& app)
|
||||
{
|
||||
pp::panopainter::open_legacy_shortcuts_dialog(app);
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
82
src/app_dialogs_workflow.cpp
Normal file
82
src/app_dialogs_workflow.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_document_open_services.h"
|
||||
#include "legacy_document_session_services.h"
|
||||
|
||||
void App::continue_document_workflow_after_optional_save(std::function<void()> action)
|
||||
{
|
||||
const bool has_canvas = canvas != nullptr;
|
||||
const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved;
|
||||
const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes);
|
||||
const auto status = pp::panopainter::execute_legacy_document_workflow_decision(
|
||||
*this,
|
||||
decision,
|
||||
std::move(action));
|
||||
if (!status.ok())
|
||||
LOG("Document workflow action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void App::dialog_newdoc()
|
||||
{
|
||||
auto show_dialog = [this] {
|
||||
pp::panopainter::open_legacy_new_document_dialog(*this);
|
||||
};
|
||||
|
||||
continue_document_workflow_after_optional_save(show_dialog);
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
void App::dialog_open()
|
||||
{
|
||||
continue_document_workflow_after_optional_save([this] {
|
||||
pp::panopainter::open_legacy_document_open_dialog(*this);
|
||||
});
|
||||
}
|
||||
|
||||
void App::dialog_browse()
|
||||
{
|
||||
continue_document_workflow_after_optional_save([this] {
|
||||
pp::panopainter::open_legacy_document_browse_dialog(*this);
|
||||
});
|
||||
}
|
||||
|
||||
void App::dialog_save_ver()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto status = pp::panopainter::execute_legacy_document_version_save_dialog(*this);
|
||||
if (!status.ok())
|
||||
LOG("Document version save action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void App::save_document(pp::app::DocumentSaveIntent intent)
|
||||
{
|
||||
const auto decision = pp::app::plan_document_save(
|
||||
Canvas::I->m_newdoc,
|
||||
Canvas::I->m_unsaved,
|
||||
intent);
|
||||
const auto status = pp::panopainter::execute_legacy_document_save_decision(*this, decision);
|
||||
if (!status.ok())
|
||||
LOG("Document save action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void App::dialog_save()
|
||||
{
|
||||
if (!check_license())
|
||||
{
|
||||
message_box("License", "This function is disabled in demo mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (canvas)
|
||||
pp::panopainter::open_legacy_document_file_save_dialog(*this);
|
||||
}
|
||||
|
||||
void App::dialog_resize()
|
||||
{
|
||||
pp::panopainter::open_legacy_document_resize_dialog(*this);
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/app_input.h"
|
||||
#include "app_core/document_platform_io.h"
|
||||
#include "app_core/document_sharing.h"
|
||||
#include "legacy_app_frame_services.h"
|
||||
#include "platform_api/platform_services.h"
|
||||
#include "platform_legacy/legacy_platform_services.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#ifdef __LINUX__
|
||||
#include "platform_linux/linux_platform_services.h"
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include "platform_windows/windows_platform_services.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] GLint rgba8_internal_format() noexcept
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
|
||||
{
|
||||
const auto action = pp::app::plan_virtual_keyboard(visible);
|
||||
@@ -33,14 +37,12 @@ namespace {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
|
||||
[[nodiscard]] pp::platform::PlatformServices& active_platform_services(const App* app)
|
||||
{
|
||||
if (App::I)
|
||||
{
|
||||
if (auto* services = App::I->platform_services())
|
||||
return *services;
|
||||
}
|
||||
return pp::platform::legacy::platform_services();
|
||||
assert(app);
|
||||
auto* services = app->platform_services();
|
||||
assert(services);
|
||||
return *services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,7 +60,7 @@ pp::platform::PlatformServices* App::platform_services() const noexcept
|
||||
|
||||
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
|
||||
{
|
||||
return active_platform_services().prepare_storage_paths();
|
||||
return active_platform_services(this).prepare_storage_paths();
|
||||
}
|
||||
|
||||
std::string App::clipboard_get_text()
|
||||
@@ -66,7 +68,7 @@ std::string App::clipboard_get_text()
|
||||
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
|
||||
return {};
|
||||
|
||||
return active_platform_services().clipboard_text();
|
||||
return active_platform_services(this).clipboard_text();
|
||||
}
|
||||
|
||||
bool App::clipboard_set_text(const std::string& s)
|
||||
@@ -74,34 +76,27 @@ bool App::clipboard_set_text(const std::string& s)
|
||||
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
|
||||
return false;
|
||||
|
||||
return active_platform_services().set_clipboard_text(s);
|
||||
return active_platform_services(this).set_clipboard_text(s);
|
||||
}
|
||||
|
||||
void App::stacktrace()
|
||||
{
|
||||
active_platform_services().log_stacktrace();
|
||||
active_platform_services(this).log_stacktrace();
|
||||
}
|
||||
|
||||
void App::crash_test()
|
||||
{
|
||||
active_platform_services().trigger_crash_test();
|
||||
active_platform_services(this).trigger_crash_test();
|
||||
}
|
||||
|
||||
void App::tick(float dt)
|
||||
{
|
||||
if (auto* main = layout_designer[main_id])
|
||||
main->tick(dt);
|
||||
if (auto* main = layout[main_id])
|
||||
main->tick(dt);
|
||||
pp::panopainter::execute_legacy_app_frame_tick(*this, dt);
|
||||
}
|
||||
|
||||
void App::resize(float w, float h)
|
||||
{
|
||||
LOG("App::resize %d %d", (int)w, (int)h);
|
||||
uirtt.create(static_cast<int>(w), static_cast<int>(h), -1, rgba8_internal_format(), true);
|
||||
redraw = true;
|
||||
width = w;
|
||||
height = h;
|
||||
pp::panopainter::execute_legacy_app_frame_resize(*this, w, h);
|
||||
}
|
||||
|
||||
void App::show_cursor()
|
||||
@@ -109,7 +104,7 @@ void App::show_cursor()
|
||||
if (!should_dispatch_cursor_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(true);
|
||||
active_platform_services(this).set_cursor_visible(true);
|
||||
}
|
||||
|
||||
void App::hide_cursor()
|
||||
@@ -117,7 +112,7 @@ void App::hide_cursor()
|
||||
if (!should_dispatch_cursor_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(false);
|
||||
active_platform_services(this).set_cursor_visible(false);
|
||||
}
|
||||
|
||||
void App::showKeyboard()
|
||||
@@ -127,7 +122,7 @@ void App::showKeyboard()
|
||||
if (!should_dispatch_keyboard_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(true);
|
||||
active_platform_services(this).set_virtual_keyboard_visible(true);
|
||||
}
|
||||
|
||||
void App::hideKeyboard()
|
||||
@@ -137,55 +132,128 @@ void App::hideKeyboard()
|
||||
if (!should_dispatch_keyboard_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(false);
|
||||
active_platform_services(this).set_virtual_keyboard_visible(false);
|
||||
}
|
||||
|
||||
void App::pick_image(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
active_platform_services().pick_image(std::move(callback));
|
||||
active_platform_services(this).pick_image(std::move(callback));
|
||||
}
|
||||
|
||||
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
active_platform_services().pick_file(std::move(types), std::move(callback));
|
||||
active_platform_services(this).pick_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
#if __IOS__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
std::string ext = "." + type;
|
||||
std::string path = tmp_path + "/" + default_name + ext;
|
||||
std::thread([=]{
|
||||
writer(path);
|
||||
save_prepared_file(path, default_name + ext, callback);
|
||||
}).detach();
|
||||
const auto target = active_platform_services(this).prepare_writable_file(type, default_name, data_path, tmp_path);
|
||||
if (target.path.empty())
|
||||
{
|
||||
callback({}, false);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("App::pick_file_save %s", target.path.c_str());
|
||||
if (target.write_on_background_thread) {
|
||||
auto* app = this;
|
||||
runtime_.prepared_file_task([
|
||||
app,
|
||||
writer = std::move(writer),
|
||||
callback = std::move(callback),
|
||||
path = target.path,
|
||||
suggested_name = target.suggested_name
|
||||
]() mutable {
|
||||
writer(path);
|
||||
app->ui_task([app,
|
||||
path = std::move(path),
|
||||
suggested_name = std::move(suggested_name),
|
||||
callback = std::move(callback)]() mutable {
|
||||
app->save_prepared_file(
|
||||
std::move(path),
|
||||
std::move(suggested_name),
|
||||
std::move(callback));
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
writer(target.path);
|
||||
save_prepared_file(target.path, target.suggested_name, std::move(callback));
|
||||
}
|
||||
#elif __WEB__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
auto path = data_path + "/" + default_name + "." + type;
|
||||
LOG("App::pick_file_save %s", path.c_str());
|
||||
writer(path);
|
||||
save_prepared_file(path, default_name + "." + type, std::move(callback));
|
||||
}
|
||||
#else
|
||||
|
||||
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
active_platform_services().pick_save_file(std::move(types), std::move(callback));
|
||||
active_platform_services(this).pick_save_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
bool App::uses_prepared_file_writes() const
|
||||
{
|
||||
return active_platform_services(this).uses_prepared_file_writes();
|
||||
}
|
||||
|
||||
bool App::uses_work_directory_document_export_collections() const
|
||||
{
|
||||
return active_platform_services(this).uses_work_directory_document_export_collections();
|
||||
}
|
||||
|
||||
bool App::disables_network_tls_verification() const
|
||||
{
|
||||
return active_platform_services(this).disables_network_tls_verification();
|
||||
}
|
||||
|
||||
bool App::uses_ppbr_export_data_directory_override() const
|
||||
{
|
||||
return active_platform_services(this).uses_ppbr_export_data_directory_override();
|
||||
}
|
||||
|
||||
bool App::platform_supports_sonarpen() const
|
||||
{
|
||||
return active_platform_services(this).supports_sonarpen();
|
||||
}
|
||||
|
||||
void App::start_platform_sonarpen()
|
||||
{
|
||||
active_platform_services(this).start_sonarpen();
|
||||
}
|
||||
|
||||
int App::default_canvas_resolution() const
|
||||
{
|
||||
return active_platform_services(this).default_canvas_resolution();
|
||||
}
|
||||
|
||||
bool App::draws_canvas_tip_for_input(kEventSource source, kEventType type) const
|
||||
{
|
||||
return active_platform_services(this).draws_canvas_tip_for_pointer(
|
||||
source == kEventSource::Mouse,
|
||||
source == kEventSource::Stylus,
|
||||
type == kEventType::MouseUpL);
|
||||
}
|
||||
|
||||
float App::adjust_canvas_input_pressure(float pressure) const
|
||||
{
|
||||
return active_platform_services(this).adjust_canvas_input_pressure(pressure);
|
||||
}
|
||||
#endif
|
||||
|
||||
void App::pick_dir(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
active_platform_services().pick_directory(std::move(callback));
|
||||
active_platform_services(this).pick_directory(std::move(callback));
|
||||
}
|
||||
|
||||
bool App::supports_working_directory_picker() const
|
||||
{
|
||||
return active_platform_services(this).supports_working_directory_picker();
|
||||
}
|
||||
|
||||
std::string App::format_working_directory_path(std::string_view path) const
|
||||
{
|
||||
return active_platform_services(this).format_working_directory_path(path);
|
||||
}
|
||||
|
||||
void App::display_file(std::string path)
|
||||
@@ -193,7 +261,7 @@ void App::display_file(std::string path)
|
||||
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
|
||||
return;
|
||||
|
||||
active_platform_services().display_file(path);
|
||||
active_platform_services(this).display_file(path);
|
||||
}
|
||||
|
||||
void App::share_file(std::string path)
|
||||
@@ -204,92 +272,131 @@ void App::share_file(std::string path)
|
||||
message_box("Sharing failed", "Please save the document before sharing it.");
|
||||
return;
|
||||
}
|
||||
active_platform_services().share_file(path);
|
||||
active_platform_services(this).share_file(path);
|
||||
}
|
||||
|
||||
void App::request_app_close()
|
||||
{
|
||||
active_platform_services().request_app_close();
|
||||
active_platform_services(this).request_app_close();
|
||||
}
|
||||
|
||||
bool App::start_platform_vr_mode()
|
||||
{
|
||||
return active_platform_services(this).start_vr_mode();
|
||||
}
|
||||
|
||||
void App::stop_platform_vr_mode()
|
||||
{
|
||||
active_platform_services(this).stop_vr_mode();
|
||||
}
|
||||
|
||||
void App::attach_ui_thread()
|
||||
{
|
||||
active_platform_services().attach_ui_thread();
|
||||
active_platform_services(this).attach_ui_thread();
|
||||
}
|
||||
|
||||
void App::detach_ui_thread()
|
||||
{
|
||||
active_platform_services().detach_ui_thread();
|
||||
active_platform_services(this).detach_ui_thread();
|
||||
}
|
||||
|
||||
void App::acquire_render_context()
|
||||
{
|
||||
active_platform_services().acquire_render_context();
|
||||
active_platform_services(this).acquire_render_context();
|
||||
}
|
||||
|
||||
void App::release_render_context()
|
||||
{
|
||||
active_platform_services().release_render_context();
|
||||
active_platform_services(this).release_render_context();
|
||||
}
|
||||
|
||||
void App::present_render_context()
|
||||
{
|
||||
active_platform_services().present_render_context();
|
||||
active_platform_services(this).present_render_context();
|
||||
}
|
||||
|
||||
void App::bind_default_render_target()
|
||||
{
|
||||
active_platform_services().bind_default_render_target();
|
||||
active_platform_services(this).bind_default_render_target();
|
||||
}
|
||||
|
||||
void App::bind_main_render_target()
|
||||
{
|
||||
active_platform_services().bind_main_render_target();
|
||||
active_platform_services(this).bind_main_render_target();
|
||||
}
|
||||
|
||||
void App::apply_render_platform_hints()
|
||||
{
|
||||
active_platform_services().apply_render_platform_hints();
|
||||
active_platform_services(this).apply_render_platform_hints();
|
||||
}
|
||||
|
||||
void App::install_render_debug_callback()
|
||||
{
|
||||
active_platform_services().install_render_debug_callback();
|
||||
active_platform_services(this).install_render_debug_callback();
|
||||
}
|
||||
|
||||
void App::begin_render_capture_frame()
|
||||
{
|
||||
active_platform_services().begin_render_capture_frame();
|
||||
active_platform_services(this).begin_render_capture_frame();
|
||||
}
|
||||
|
||||
void App::end_render_capture_frame()
|
||||
{
|
||||
active_platform_services().end_render_capture_frame();
|
||||
active_platform_services(this).end_render_capture_frame();
|
||||
}
|
||||
|
||||
bool App::platform_deletes_recorded_files_on_clear()
|
||||
{
|
||||
return active_platform_services().deletes_recorded_files_on_clear();
|
||||
return active_platform_services(this).deletes_recorded_files_on_clear();
|
||||
}
|
||||
|
||||
void App::clear_platform_recorded_files(std::string path)
|
||||
{
|
||||
active_platform_services().clear_recorded_files(path);
|
||||
active_platform_services(this).clear_recorded_files(path);
|
||||
}
|
||||
|
||||
void App::publish_exported_image(std::string path)
|
||||
{
|
||||
active_platform_services(this).publish_exported_image(path);
|
||||
}
|
||||
|
||||
void App::flush_platform_storage()
|
||||
{
|
||||
active_platform_services(this).flush_persistent_storage();
|
||||
}
|
||||
|
||||
std::vector<std::string> App::document_browse_roots() const
|
||||
{
|
||||
return active_platform_services(this).document_browse_roots(work_path, data_path);
|
||||
}
|
||||
|
||||
void App::save_platform_ui_state()
|
||||
{
|
||||
active_platform_services(this).save_ui_state();
|
||||
}
|
||||
|
||||
bool App::platform_enables_live_asset_reloading()
|
||||
{
|
||||
return active_platform_services().enables_live_asset_reloading();
|
||||
return active_platform_services(this).enables_live_asset_reloading();
|
||||
}
|
||||
|
||||
void App::update_platform_frame(float delta_time_seconds)
|
||||
{
|
||||
active_platform_services().update_platform_frame(delta_time_seconds);
|
||||
active_platform_services(this).update_platform_frame(delta_time_seconds);
|
||||
}
|
||||
|
||||
void App::report_rendered_frames(int frames)
|
||||
{
|
||||
active_platform_services().report_rendered_frames(frames);
|
||||
active_platform_services(this).report_rendered_frames(frames);
|
||||
}
|
||||
|
||||
VrSessionSnapshot App::vr_session_snapshot() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return pp::platform::windows::read_platform_vr_session_snapshot();
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
void App::save_prepared_file(
|
||||
@@ -297,7 +404,7 @@ void App::save_prepared_file(
|
||||
std::string suggested_name,
|
||||
std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
active_platform_services().save_prepared_file(
|
||||
active_platform_services(this).save_prepared_file(
|
||||
path,
|
||||
suggested_name,
|
||||
[callback = std::move(callback)](std::string saved_path, bool saved) {
|
||||
@@ -307,179 +414,284 @@ void App::save_prepared_file(
|
||||
|
||||
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse down dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseDownR : kEventType::MouseDownL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_move(float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
cursor = { x / zoom, y / zoom };
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
cursor = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseMove;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_up(int button, float x, float y, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse up dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseUpR : kEventType::MouseUpL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_scroll(float x, float y, float delta)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse scroll dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseScroll;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_scroll_delta = delta;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_cancel(int button)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_mouse_cancel_dispatch(
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseCancel;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_start(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture start dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureStart;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
gesture_p0 = p0;
|
||||
gesture_p1 = p1;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_move(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
gesture_p0.x,
|
||||
gesture_p0.y,
|
||||
gesture_p1.x,
|
||||
gesture_p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureMove;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_distance_delta = e.m_distance - glm::distance(gesture_p0, gesture_p1);
|
||||
e.m_pos_delta = p - glm::lerp(gesture_p0, gesture_p1, 0.5f);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
e.m_distance_delta = plan.value().distance_delta;
|
||||
e.m_pos_delta = { plan.value().position_delta_x, plan.value().position_delta_y };
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_end()
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
GestureEvent e;
|
||||
e.m_type = kEventType::GestureEnd;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::touch_tap(const glm::vec2& pos, int fingers, int tap_count)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
TouchEvent e;
|
||||
e.m_type = kEventType::TouchTap;
|
||||
e.m_finger_count = fingers;
|
||||
e.m_tap_count = tap_count;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_down(kKey key)
|
||||
{
|
||||
if (key == kKey::KeySpacebar && vr_active)
|
||||
const auto vr_session = vr_session_snapshot();
|
||||
const auto plan = pp::app::plan_app_key_down_dispatch(
|
||||
layout.get(main_id) != nullptr,
|
||||
key == kKey::KeySpacebar,
|
||||
vr_session.vr_active);
|
||||
if (plan.sync_vr_camera_rotation)
|
||||
canvas->m_canvas->m_cam_rot = vr_rot;
|
||||
redraw = true;
|
||||
keys[(int)key] = true;
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyDown;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_up(kKey key)
|
||||
{
|
||||
redraw = true;
|
||||
keys[(int)key] = false;
|
||||
const auto plan = pp::app::plan_app_key_up_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyUp;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_char(char key)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyChar;
|
||||
e.m_char = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
|
||||
void App::toggle_ui()
|
||||
{
|
||||
auto m = layout[main_id]->m_children[1];
|
||||
ui_visible = !ui_visible;
|
||||
for (int i = 1; i < m->m_children.size(); i++)
|
||||
m->m_children[i]->m_display = ui_visible;
|
||||
auto* main = layout[main_id];
|
||||
const std::size_t main_child_count = main ? main->m_children.size() : 0U;
|
||||
auto* panel_container = main_child_count > 1U ? main->m_children[1].get() : nullptr;
|
||||
const auto plan = pp::app::plan_app_ui_visibility_toggle(
|
||||
ui_visible,
|
||||
main != nullptr,
|
||||
main_child_count,
|
||||
panel_container ? panel_container->m_children.size() : 0U);
|
||||
if (!plan) {
|
||||
LOG("UI toggle plan failed: %s", plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_visible = plan.value().next_ui_visible;
|
||||
if (!panel_container)
|
||||
return;
|
||||
|
||||
for (std::size_t i = plan.value().first_panel_child_index;
|
||||
i < plan.value().panel_child_count;
|
||||
++i) {
|
||||
panel_container->m_children[i]->m_display = ui_visible;
|
||||
}
|
||||
}
|
||||
|
||||
void App::set_stylus()
|
||||
{
|
||||
has_stylus = true;
|
||||
if (canvas)
|
||||
const auto plan = pp::app::plan_app_stylus_attach(canvas != nullptr);
|
||||
has_stylus = plan.set_has_stylus;
|
||||
if (plan.enable_canvas_touch_lock && canvas)
|
||||
canvas->m_canvas->m_touch_lock = true;
|
||||
}
|
||||
|
||||
2537
src/app_layout.cpp
2537
src/app_layout.cpp
File diff suppressed because it is too large
Load Diff
166
src/app_layout_about_layer_menu.cpp
Normal file
166
src/app_layout_about_layer_menu.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/document_layer.h"
|
||||
#include "legacy_about_menu_binding_services.h"
|
||||
#include "legacy_document_layer_services.h"
|
||||
#include "legacy_ui_overlay_services.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "node_popup_menu.h"
|
||||
#include "node_text.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::shared_ptr<NodePopupMenu> add_menu_popup(
|
||||
App& app,
|
||||
const char* template_id,
|
||||
glm::vec2 position,
|
||||
float rtl_anchor_width)
|
||||
{
|
||||
const auto popup = pp::panopainter::add_legacy_popup_menu(
|
||||
app,
|
||||
template_id,
|
||||
position.x,
|
||||
position.y,
|
||||
rtl_anchor_width);
|
||||
if (!popup) {
|
||||
LOG("Popup menu '%s' failed: %s", template_id ? template_id : "<null>", popup.status().message);
|
||||
return nullptr;
|
||||
}
|
||||
return popup.value();
|
||||
}
|
||||
|
||||
void close_legacy_overlay_handle_ignoring_status(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle overlay) noexcept
|
||||
{
|
||||
(void)pp::panopainter::close_legacy_overlay_node(anchor, overlay);
|
||||
}
|
||||
|
||||
pp::app::DocumentLayerMenuPlan make_layer_menu_plan(
|
||||
pp::app::DocumentLayerMenuCommand command,
|
||||
App& app)
|
||||
{
|
||||
const bool has_current_layer = app.layers && app.layers->m_current_layer;
|
||||
const int current_index = app.canvas && app.canvas->m_canvas
|
||||
? app.canvas->m_canvas->m_current_layer_idx
|
||||
: 0;
|
||||
const int animation_duration = Canvas::I
|
||||
? Canvas::I->anim_duration()
|
||||
: 0;
|
||||
const std::string current_name = has_current_layer
|
||||
? app.layers->m_current_layer->m_label_text
|
||||
: std::string {};
|
||||
std::string lower_name;
|
||||
if (app.canvas && app.canvas->m_canvas && current_index > 0
|
||||
&& current_index - 1 < static_cast<int>(app.canvas->m_canvas->m_layers.size()))
|
||||
{
|
||||
lower_name = app.canvas->m_canvas->m_layers[current_index - 1]->m_name;
|
||||
}
|
||||
|
||||
const auto plan = pp::app::plan_document_layer_menu(
|
||||
command,
|
||||
has_current_layer,
|
||||
current_index,
|
||||
animation_duration,
|
||||
current_name,
|
||||
lower_name);
|
||||
if (plan)
|
||||
return plan.value();
|
||||
return {};
|
||||
}
|
||||
|
||||
void execute_document_layer_menu_plan(App& app, const pp::app::DocumentLayerMenuPlan& plan)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_document_layer_menu_plan(app, plan);
|
||||
if (!status.ok())
|
||||
LOG("Layer menu action failed: %s", status.message);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void bind_legacy_about_menu(App& app)
|
||||
{
|
||||
auto main = app.layout[app.main_id];
|
||||
|
||||
if (auto* menu_file = main->find<NodeButtonCustom>("menu-about"))
|
||||
{
|
||||
auto* popup_root = app.layout[app.main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
pp::panopainter::bind_legacy_about_menu_popup(
|
||||
app,
|
||||
*popup_root,
|
||||
*menu_file,
|
||||
g_version_major,
|
||||
g_version_minor,
|
||||
g_version_fix,
|
||||
app.canvas && app.canvas->m_canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void bind_legacy_layer_menu(App& app)
|
||||
{
|
||||
auto main = app.layout[app.main_id];
|
||||
|
||||
if (auto* menu_file = main->find<NodeButtonCustom>("menu-layers"))
|
||||
{
|
||||
menu_file->on_click = [&app, menu_file](Node*) {
|
||||
auto* popup_root = app.layout[app.main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
glm::vec2 pos = menu_file->m_pos + glm::vec2(0, menu_file->m_size.y);
|
||||
auto popup = add_menu_popup(app, "layers-menu", pos, menu_file->m_size.x);
|
||||
if (!popup)
|
||||
return;
|
||||
pp::panopainter::detach_legacy_node_from_parent(*popup);
|
||||
auto popup_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_root, popup);
|
||||
if (!popup_overlay) {
|
||||
pp::panopainter::destroy_legacy_node(*popup);
|
||||
return;
|
||||
}
|
||||
auto popup_handle = popup_overlay.value();
|
||||
|
||||
popup->find<NodeButtonCustom>("layer-clear")->on_click = [&app, popup_root, popup_handle](Node*) {
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::clear, app);
|
||||
execute_document_layer_menu_plan(app, plan);
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_root, popup_handle);
|
||||
};
|
||||
{
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::clear, app);
|
||||
popup->find<NodeButtonCustom>("layer-clear")->
|
||||
find<NodeText>("menu-label")->
|
||||
set_text(plan.label.c_str());
|
||||
}
|
||||
|
||||
popup->find<NodeButtonCustom>("layer-rename")->on_click = [&app, popup_root, popup_handle](Node*) {
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::rename, app);
|
||||
execute_document_layer_menu_plan(app, plan);
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_root, popup_handle);
|
||||
};
|
||||
{
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::rename, app);
|
||||
popup->find<NodeButtonCustom>("layer-rename")->
|
||||
find<NodeText>("menu-label")->
|
||||
set_text(plan.label.c_str());
|
||||
}
|
||||
|
||||
popup->find<NodeButtonCustom>("layer-merge")->on_click = [&app, popup_root, popup_handle](Node*) {
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::merge_down, app);
|
||||
execute_document_layer_menu_plan(app, plan);
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_root, popup_handle);
|
||||
};
|
||||
{
|
||||
const auto plan = make_layer_menu_plan(pp::app::DocumentLayerMenuCommand::merge_down, app);
|
||||
popup->find<NodeButtonCustom>("layer-merge")->
|
||||
find<NodeText>("menu-label")->
|
||||
set_text(plan.label.c_str());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
148
src/app_layout_bootstrap.cpp
Normal file
148
src/app_layout_bootstrap.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "node_border.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "node_icon.h"
|
||||
#include "node_image.h"
|
||||
#include "node_shorcuts.h"
|
||||
#include "node_stroke_preview.h"
|
||||
#include "node_text.h"
|
||||
#include "app_core/app_status.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::shared_ptr<Layer>> saved_layers;
|
||||
|
||||
void capture_layout_reload_state(App& app)
|
||||
{
|
||||
saved_layers = std::move(Canvas::I->m_layers);
|
||||
app.ui_save();
|
||||
NodeStrokePreview::empty_queue();
|
||||
}
|
||||
|
||||
void apply_layout_loaded_state(App& app, bool reloaded)
|
||||
{
|
||||
LOG("initializing layout updating after load %d x %d zoom %f", (int)app.width, (int)app.height, app.zoom);
|
||||
app.layout[app.main_id]->update(app.width, app.height, app.zoom);
|
||||
|
||||
LOG("initializing layout components");
|
||||
app.init_sidebar();
|
||||
|
||||
if (reloaded) {
|
||||
for (const auto& layer : saved_layers)
|
||||
app.layers->add_layer(layer->m_name.c_str(), false, true, layer);
|
||||
} else {
|
||||
app.layers->add_layer("Default", false, true);
|
||||
Canvas::I->m_unsaved = false;
|
||||
}
|
||||
|
||||
app.init_toolbar_draw();
|
||||
app.init_toolbar_main();
|
||||
app.init_menu_file();
|
||||
app.init_menu_edit();
|
||||
app.init_menu_layer();
|
||||
app.init_menu_tools();
|
||||
app.init_menu_about();
|
||||
|
||||
if (auto* version_label = app.layout[app.main_id]->find<NodeText>("version")) {
|
||||
version_label->set_text(g_version);
|
||||
}
|
||||
|
||||
const auto renderer_features = ShaderManager::render_device_features();
|
||||
const auto renderer_diagnostics = pp::app::plan_renderer_diagnostics({
|
||||
.framebuffer_fetch = renderer_features.framebuffer_fetch,
|
||||
.float32_render_targets = renderer_features.float32_render_targets,
|
||||
.float32_linear_filtering = renderer_features.float32_linear_filtering,
|
||||
.float16_render_targets = renderer_features.float16_render_targets,
|
||||
});
|
||||
|
||||
if (auto* indicator = app.layout[app.main_id]->find<NodeBorder>("ext-fbf")) {
|
||||
indicator->m_color = renderer_diagnostics.framebuffer_fetch.supported
|
||||
? glm::vec4(0, 1, 0, 1)
|
||||
: glm::vec4(1, 0, 0, 1);
|
||||
}
|
||||
|
||||
if (auto* indicator = app.layout[app.main_id]->find<NodeBorder>("ext-flt")) {
|
||||
if (renderer_diagnostics.floating_point_targets.supported) {
|
||||
if (auto* text = indicator->find<NodeText>("ext-flt-text"))
|
||||
text->set_text(std::string(renderer_diagnostics.floating_point_targets.label));
|
||||
indicator->m_color = glm::vec4(0, 1, 0, 1);
|
||||
} else {
|
||||
indicator->m_color = glm::vec4(1, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
app.dialog_whatsnew(false);
|
||||
app.brush_update(true, true);
|
||||
|
||||
if (auto* toolbar = app.layout[app.main_id]->find<Node>("toolbar"))
|
||||
toolbar->m_flood_events = true;
|
||||
|
||||
auto* toggle_button = new NodeImage;
|
||||
toggle_button->m_path = "data/ui/p-black.png";
|
||||
toggle_button->m_tex_id = const_hash("data/ui/p-black.png");
|
||||
toggle_button->SetSize(30, 45);
|
||||
toggle_button->create();
|
||||
|
||||
auto* button = new NodeButtonCustom;
|
||||
button->create();
|
||||
button->add_child(toggle_button);
|
||||
button->SetPositioning(YGPositionTypeAbsolute);
|
||||
button->set_color({ 0, 0, 0, 0 });
|
||||
YGNodeStyleSetPosition(button->y_node, YGEdgeBottom, 8);
|
||||
YGNodeStyleSetPosition(button->y_node, YGEdgeLeft, 10);
|
||||
app.layout[app.main_id]->add_child(button);
|
||||
|
||||
button->on_click = [&app](Node*) {
|
||||
app.toggle_ui();
|
||||
};
|
||||
|
||||
app.ui_restore();
|
||||
app.redraw = true;
|
||||
}
|
||||
|
||||
void init_layout_designer(App& app)
|
||||
{
|
||||
app.layout_designer.on_loaded = [&app](bool) {
|
||||
app.layout_designer.create();
|
||||
auto* shortcuts = app.layout_designer[app.main_id]->add_child<NodeShortcuts>();
|
||||
(void)shortcuts;
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void init_layout_bootstrap(App& app)
|
||||
{
|
||||
LOG("initializing layout statics");
|
||||
NodeBorder::static_init();
|
||||
NodeImage::static_init();
|
||||
NodeIcon::static_init();
|
||||
NodeStrokePreview::static_init();
|
||||
|
||||
app.layout.on_reloading = [&app] {
|
||||
capture_layout_reload_state(app);
|
||||
};
|
||||
|
||||
app.layout.on_loaded = [&app](bool reloaded) {
|
||||
apply_layout_loaded_state(app, reloaded);
|
||||
};
|
||||
|
||||
LOG("initializing layout xml");
|
||||
if (app.layout.m_loaded) {
|
||||
LOG("restore layout");
|
||||
app.layout.restore_context();
|
||||
} else {
|
||||
app.layout.load("data/layout.xml");
|
||||
}
|
||||
LOG("initializing layout completed");
|
||||
|
||||
LOG("initializing layout designer xml");
|
||||
init_layout_designer(app);
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
61
src/app_layout_brush.cpp
Normal file
61
src/app_layout_brush.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
|
||||
void App::brush_update(bool update_color, bool update_brush)
|
||||
{
|
||||
// brushes->select_brush(canvas->m_brush->id);
|
||||
// stroke->set_params(canvas->m_brush);
|
||||
render_task_async([this, update_color, update_brush]
|
||||
{
|
||||
pp::app::BrushUiRefreshInput input;
|
||||
input.update_color = update_color;
|
||||
input.update_brush = update_brush;
|
||||
auto current_brush = Canvas::I ? Canvas::I->m_current_brush : nullptr;
|
||||
input.has_current_brush = current_brush != nullptr;
|
||||
input.has_floating_picker = floating_picker != nullptr;
|
||||
input.has_floating_color_panel = floating_color != nullptr;
|
||||
if (input.has_current_brush)
|
||||
{
|
||||
input.tip_flow = current_brush->m_tip_flow;
|
||||
input.tip_size = current_brush->m_tip_size;
|
||||
input.r = current_brush->m_tip_color.r;
|
||||
input.g = current_brush->m_tip_color.g;
|
||||
input.b = current_brush->m_tip_color.b;
|
||||
input.a = current_brush->m_tip_color.a;
|
||||
}
|
||||
|
||||
const auto view = pp::app::plan_brush_ui_refresh(input);
|
||||
if (!view) {
|
||||
LOG("Brush UI refresh failed: %s", view.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.value().updates_stroke_controls)
|
||||
{
|
||||
stroke->update_controls();
|
||||
}
|
||||
if (view.value().updates_quick_flow)
|
||||
{
|
||||
quick->m_slider_flow->set_value(stroke->m_tip_flow->get_value());
|
||||
}
|
||||
if (view.value().updates_quick_size)
|
||||
{
|
||||
quick->m_slider_size->set_value(stroke->m_tip_size->get_value());
|
||||
}
|
||||
if (view.value().updates_quick_brush_preview && current_brush)
|
||||
{
|
||||
*quick->m_button_brush_current_preview->m_brush = *current_brush;
|
||||
quick->m_button_brush_current_preview->draw_stroke();
|
||||
}
|
||||
if (view.value().updates_quick_color)
|
||||
{
|
||||
const glm::vec4 color(view.value().r, view.value().g, view.value().b, view.value().a);
|
||||
quick->m_button_color_current_inner->m_color = color;
|
||||
if (view.value().updates_floating_picker)
|
||||
floating_picker->set_color(color);
|
||||
if (view.value().updates_floating_color_panel)
|
||||
floating_color->set_color(color);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
8
src/app_layout_draw_toolbar.cpp
Normal file
8
src/app_layout_draw_toolbar.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_draw_toolbar_binding_services.h"
|
||||
|
||||
void App::init_toolbar_draw()
|
||||
{
|
||||
pp::panopainter::bind_legacy_draw_toolbar_buttons(*this, *layout[main_id]);
|
||||
}
|
||||
47
src/app_layout_edit_menu.cpp
Normal file
47
src/app_layout_edit_menu.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_ui_overlay_services.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "node_popup_menu.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::shared_ptr<NodePopupMenu> add_menu_popup(
|
||||
App& app,
|
||||
const char* template_id,
|
||||
glm::vec2 position,
|
||||
float rtl_anchor_width)
|
||||
{
|
||||
const auto popup = pp::panopainter::add_legacy_popup_menu(
|
||||
app,
|
||||
template_id,
|
||||
position.x,
|
||||
position.y,
|
||||
rtl_anchor_width);
|
||||
if (!popup) {
|
||||
LOG("Popup menu '%s' failed: %s", template_id ? template_id : "<null>", popup.status().message);
|
||||
return nullptr;
|
||||
}
|
||||
return popup.value();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void bind_legacy_edit_menu(App& app)
|
||||
{
|
||||
auto main = app.layout[app.main_id];
|
||||
|
||||
if (auto* menu_edit = main->find<NodeButtonCustom>("menu-edit"))
|
||||
{
|
||||
menu_edit->on_click = [&app, menu_edit](Node*) {
|
||||
glm::vec2 pos = menu_edit->m_pos + glm::vec2(0, menu_edit->m_size.y);
|
||||
auto popup = add_menu_popup(app, "edit-menu", pos, menu_edit->m_size.x);
|
||||
if (!popup)
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
18
src/app_layout_file_menu.cpp
Normal file
18
src/app_layout_file_menu.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_file_menu_binding_services.h"
|
||||
#include "node_button_custom.h"
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void bind_legacy_file_menu(App& app)
|
||||
{
|
||||
auto main = app.layout[app.main_id];
|
||||
|
||||
if (auto* menu_file = main->find<NodeButtonCustom>("menu-file"))
|
||||
{
|
||||
bind_legacy_file_menu_popup(app, *menu_file, *main);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
12
src/app_layout_main_toolbar.cpp
Normal file
12
src/app_layout_main_toolbar.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_main_toolbar_binding_services.h"
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void bind_legacy_main_toolbar(App& app)
|
||||
{
|
||||
pp::panopainter::bind_legacy_main_toolbar_buttons(app, *app.layout[app.main_id]);
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
328
src/app_layout_sidebar.cpp
Normal file
328
src/app_layout_sidebar.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "node_panel_floating.h"
|
||||
#include "app_core/brush_ui.h"
|
||||
#include "app_core/document_layer.h"
|
||||
#include "legacy_sidebar_grid_popup_services.h"
|
||||
#include "legacy_sidebar_color_popup_services.h"
|
||||
#include "legacy_sidebar_stroke_popup_services.h"
|
||||
#include "legacy_brush_ui_services.h"
|
||||
#include "legacy_document_layer_services.h"
|
||||
#include "legacy_ui_overlay_services.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool apply_brush_color_plan(App& app, glm::vec4 color, bool update_quick, bool update_color_panel)
|
||||
{
|
||||
return pp::panopainter::apply_legacy_brush_color_plan(app, color, update_quick, update_color_panel);
|
||||
}
|
||||
|
||||
bool apply_brush_texture_plan(App& app, pp::app::BrushUiTextureSlot slot, const std::string& path, const std::string& thumb)
|
||||
{
|
||||
return pp::panopainter::apply_legacy_brush_texture_plan(app, slot, path, thumb);
|
||||
}
|
||||
|
||||
bool apply_brush_preset_plan(App& app, const std::shared_ptr<Brush>& brush)
|
||||
{
|
||||
return pp::panopainter::apply_legacy_brush_preset_plan(app, brush);
|
||||
}
|
||||
|
||||
void execute_document_layer_operation_plan(
|
||||
App& app,
|
||||
const pp::app::DocumentLayerOperationPlan& plan,
|
||||
const std::shared_ptr<class Layer>& pending_layer = nullptr)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_document_layer_operation_plan(app, plan, pending_layer);
|
||||
if (!status.ok())
|
||||
LOG("Layer operation failed: %s", status.message);
|
||||
}
|
||||
|
||||
void close_legacy_overlay_handle_ignoring_status(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle overlay) noexcept
|
||||
{
|
||||
(void)pp::panopainter::close_legacy_overlay_node(anchor, overlay);
|
||||
}
|
||||
|
||||
template <typename PopupOverlay, typename TickOverlay>
|
||||
void close_legacy_overlay_handles_if_open(
|
||||
Node& anchor,
|
||||
const PopupOverlay& popup_overlay,
|
||||
const TickOverlay& tick_overlay) noexcept
|
||||
{
|
||||
if (popup_overlay) {
|
||||
close_legacy_overlay_handle_ignoring_status(anchor, popup_overlay.value());
|
||||
}
|
||||
if (tick_overlay) {
|
||||
close_legacy_overlay_handle_ignoring_status(anchor, tick_overlay.value());
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::shared_ptr<T> create_panel(LayoutManager& manager)
|
||||
{
|
||||
auto ret = std::make_shared<T>();
|
||||
ret->set_manager(&manager);
|
||||
ret->init();
|
||||
ret->create();
|
||||
ret->loaded();
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void App::init_sidebar()
|
||||
{
|
||||
sidebar = layout[main_id]->find<NodeBorder>("sidebar");
|
||||
canvas = layout[main_id]->find<NodeCanvas>("paint-canvas");
|
||||
quick = layout[main_id]->find<NodePanelQuick>("panel-quick");
|
||||
floatings_container = layout[main_id]->find<Node>("floatings");
|
||||
|
||||
//brushes = layout[main_id]->find<NodePanelBrush>("panel-brush");
|
||||
//layers = layout[main_id]->find<NodePanelLayer>("panel-layer");
|
||||
//color = layout[main_id]->find<NodePanelColor>("panel-color");
|
||||
//stroke = layout[main_id]->find<NodePanelStroke>("panel-stroke");
|
||||
|
||||
//brushes = find_or_create_panel<NodePanelBrush>(panels);
|
||||
layers = create_panel<NodePanelLayer>(layout);
|
||||
color = create_panel<NodePanelColor>(layout);
|
||||
stroke = create_panel<NodePanelStroke>(layout);
|
||||
grid = create_panel<NodePanelGrid>(layout);
|
||||
presets = create_panel<NodePanelBrushPreset>(layout);
|
||||
animation = create_panel<NodePanelAnimation>(layout);
|
||||
//presets = find_or_create_panel<NodePanelBrushPreset>(panels);
|
||||
|
||||
canvas->m_canvas->on_mode_changed = [this](kCanvasMode prev, kCanvasMode mode) {
|
||||
quick_mode_state[prev] = quick->get_state();
|
||||
if (quick_mode_state.find(mode) != quick_mode_state.end())
|
||||
quick->set_state(quick_mode_state[mode], true);
|
||||
else
|
||||
quick->reset_state(true);
|
||||
brush_update(true, true);
|
||||
};
|
||||
color->on_color_changed = [this](Node* target, glm::vec4 color) {
|
||||
apply_brush_color_plan(*this, color, true, false);
|
||||
};
|
||||
|
||||
stroke->on_brush_changed = [this](Node* target, const std::string& path, const std::string& thumb) {
|
||||
apply_brush_texture_plan(*this, pp::app::BrushUiTextureSlot::tip, path, thumb);
|
||||
};
|
||||
stroke->on_pattern_changed = [this](Node* target, const std::string& path, const std::string& thumb) {
|
||||
apply_brush_texture_plan(*this, pp::app::BrushUiTextureSlot::pattern, path, thumb);
|
||||
};
|
||||
stroke->on_dual_changed = [this](Node* target, const std::string& path, const std::string& thumb) {
|
||||
apply_brush_texture_plan(*this, pp::app::BrushUiTextureSlot::dual, path, thumb);
|
||||
};
|
||||
stroke->on_stroke_change = [this](Node*) {
|
||||
const auto status = pp::panopainter::execute_legacy_brush_stroke_changed_plan(*this);
|
||||
if (!status.ok())
|
||||
LOG("Brush stroke settings action failed: %s", status.message);
|
||||
};
|
||||
|
||||
quick->on_color_change = [this](Node*, glm::vec3 c) {
|
||||
apply_brush_color_plan(*this, glm::vec4(c, 1.f), false, true);
|
||||
};
|
||||
quick->on_flow_change = [this](Node*, float value) {
|
||||
stroke->set_flow(value, true, true);
|
||||
};
|
||||
quick->on_size_change = [this](Node*, float value) {
|
||||
stroke->set_size(value, true, true);
|
||||
};
|
||||
quick->on_brush_change = [this](Node*, std::shared_ptr<Brush> b) {
|
||||
apply_brush_preset_plan(*this, b);
|
||||
};
|
||||
|
||||
layers->on_layer_add = [this](Node*, std::shared_ptr<class Layer> layer, int index) {
|
||||
const auto plan = pp::app::plan_document_layer_add(
|
||||
static_cast<int>(Canvas::I->m_layers.size()),
|
||||
index,
|
||||
layers->m_layers.back()->m_label_text);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value(), layer);
|
||||
};
|
||||
|
||||
layers->on_layer_duplicate = [this](Node*, int source_index) {
|
||||
const auto plan = pp::app::plan_document_layer_duplicate(
|
||||
static_cast<int>(Canvas::I->m_layers.size()),
|
||||
source_index);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_change = [this](Node*, int, int new_idx) {
|
||||
const auto plan = pp::app::plan_document_layer_select(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
new_idx);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_order = [this](Node*, int old_idx, int new_idx) {
|
||||
const auto plan = pp::app::plan_document_layer_reorder(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
old_idx,
|
||||
new_idx);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_delete = [this](Node*, int idx) {
|
||||
const auto plan = pp::app::plan_document_layer_remove(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_opacity_changed = [this](Node*, int idx, float value) {
|
||||
const auto plan = pp::app::plan_document_layer_opacity(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
value);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_visibility_changed = [this](Node*, int idx, bool visible) {
|
||||
const auto plan = pp::app::plan_document_layer_visibility(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
visible);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_alpha_lock_changed = [this](Node*, int idx, bool locked) {
|
||||
const auto plan = pp::app::plan_document_layer_alpha_lock(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
locked);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_blend_mode_changed = [this](Node*, int idx, int mode) {
|
||||
const auto plan = pp::app::plan_document_layer_blend_mode(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
mode);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
|
||||
layers->on_layer_highlight_changed = [this](Node*, int idx, bool highlight) {
|
||||
const auto plan = pp::app::plan_document_layer_highlight(
|
||||
static_cast<int>(canvas->m_canvas->m_layers.size()),
|
||||
idx,
|
||||
highlight);
|
||||
if (!plan)
|
||||
return;
|
||||
execute_document_layer_operation_plan(*this, plan.value());
|
||||
};
|
||||
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-stroke"))
|
||||
{
|
||||
button->on_click = [this, button](Node*) {
|
||||
auto* popup_root = layout[main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
pp::panopainter::open_legacy_sidebar_stroke_popup(*this, *popup_root, *button, *stroke);
|
||||
};
|
||||
}
|
||||
//if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-brush"))
|
||||
//{
|
||||
// button->on_click = [this, button](Node*) {
|
||||
// panels->get_child_index(brushes.get()) == -1 ? panels->add_child(brushes) : panels->remove_child(brushes.get());
|
||||
// panels->fix_scroll();
|
||||
// button->set_color(panels->get_child_index(brushes.get()) == -1 ? color_button_normal : color_button_hlight);
|
||||
// };
|
||||
//}
|
||||
//if (auto* button = layout[main_id]->find<NodeButton>("btn-brush-preset"))
|
||||
//{
|
||||
// button->on_click = [this, button](Node*) {
|
||||
// panels->get_child_index(presets.get()) == -1 ? panels->add_child(presets) : panels->remove_child(presets.get());
|
||||
// panels->fix_scroll();
|
||||
// button->set_color(panels->get_child_index(presets.get()) == -1 ? color_button_normal : color_button_hlight);
|
||||
// };
|
||||
//}
|
||||
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-color"))
|
||||
{
|
||||
button->on_click = [this, button](Node*) {
|
||||
auto* popup_root = layout[main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
pp::panopainter::open_legacy_sidebar_color_popup(*this, *popup_root, *button, *color);
|
||||
};
|
||||
}
|
||||
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-layer"))
|
||||
{
|
||||
button->on_click = [this, button](Node*) {
|
||||
auto* popup_root = layout[main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
auto screen = popup_root->m_size;
|
||||
glm::vec2 pos = button->m_pos + glm::vec2(button->m_size.x * 0.5f, button->m_size.y);
|
||||
layers->find("title")->SetVisibility(true);
|
||||
layers->SetSize(350, YGUndefined);
|
||||
if (layers->m_parent)
|
||||
{
|
||||
if (auto fp = dynamic_cast<NodePanelFloating*>(layers->m_parent->m_parent))
|
||||
{
|
||||
pp::panopainter::detach_legacy_node_from_parent(*layers);
|
||||
pp::panopainter::close_legacy_dialog_node(*fp);
|
||||
}
|
||||
}
|
||||
const auto popup_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_root, layers);
|
||||
if (!popup_overlay) {
|
||||
return;
|
||||
}
|
||||
auto tick = pp::panopainter::make_legacy_overlay_node_for_anchor<NodeImage>(*popup_root);
|
||||
tick->SetPositioning(YGPositionTypeAbsolute);
|
||||
tick->SetSize(32, 16);
|
||||
tick->SetPosition(pos.x - 16, pos.y);
|
||||
tick->set_image("data/ui/popup-tick-up.png");
|
||||
auto tick_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_root, tick);
|
||||
if (!popup_overlay || !tick_overlay)
|
||||
{
|
||||
close_legacy_overlay_handles_if_open(*popup_root, popup_overlay, tick_overlay);
|
||||
return;
|
||||
}
|
||||
const auto popup_handle = popup_overlay.value();
|
||||
const auto tick_handle = tick_overlay.value();
|
||||
popup_root->update();
|
||||
|
||||
layers->SetPosition(pos.x - layers->m_size.x / 2.f, pos.y + 16);
|
||||
layers->SetPositioning(YGPositionTypeAbsolute);
|
||||
pp::panopainter::activate_legacy_popup_overlay(*layers);
|
||||
auto scroll = layers->find<NodeScroll>("layers-container");
|
||||
scroll->SetMaxHeight(glm::max(100.f, screen.y - pos.y - 200.f));
|
||||
layers->on_popup_close = [popup_root, popup_handle, tick_handle](Node*) {
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_root, popup_handle);
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_root, tick_handle);
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
if (auto* button = layout[main_id]->find<NodeButtonCustom>("btn-grids-panel"))
|
||||
{
|
||||
button->on_click = [this, button](Node*) {
|
||||
auto* popup_root = layout[main_id];
|
||||
if (!popup_root) {
|
||||
return;
|
||||
}
|
||||
pp::panopainter::open_legacy_sidebar_grid_popup(*popup_root, *button, *grid);
|
||||
};
|
||||
}
|
||||
}
|
||||
287
src/app_layout_tools_menu.cpp
Normal file
287
src/app_layout_tools_menu.cpp
Normal file
@@ -0,0 +1,287 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "app_core/tools_menu.h"
|
||||
#include "legacy_app_preference_services.h"
|
||||
#include "legacy_app_shell_services.h"
|
||||
#include "legacy_preference_storage.h"
|
||||
#include "legacy_tools_menu_binding_services.h"
|
||||
#include "legacy_ui_overlay_services.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "node_checkbox.h"
|
||||
#include "node_combobox.h"
|
||||
#include "node_popup_menu.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::shared_ptr<NodePopupMenu> add_menu_popup(
|
||||
App& app,
|
||||
const char* template_id,
|
||||
glm::vec2 position,
|
||||
float rtl_anchor_width)
|
||||
{
|
||||
const auto popup = pp::panopainter::add_legacy_popup_menu(
|
||||
app,
|
||||
template_id,
|
||||
position.x,
|
||||
position.y,
|
||||
rtl_anchor_width);
|
||||
if (!popup) {
|
||||
LOG("Popup menu '%s' failed: %s", template_id ? template_id : "<null>", popup.status().message);
|
||||
return nullptr;
|
||||
}
|
||||
return popup.value();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace pp::panopainter {
|
||||
|
||||
void bind_legacy_tools_menu(App& app)
|
||||
{
|
||||
auto main = app.layout[app.main_id];
|
||||
|
||||
if (auto menu_exp = main->find<NodeButtonCustom>("menu-tools"))
|
||||
{
|
||||
menu_exp->on_click = [&app, menu_exp, main](Node*) {
|
||||
glm::vec2 pos = menu_exp->m_pos + glm::vec2(0, menu_exp->m_size.y);
|
||||
auto popup_exp = add_menu_popup(app, "tools-menu", pos, menu_exp->m_size.x);
|
||||
if (!popup_exp)
|
||||
return;
|
||||
|
||||
if (auto tick = popup_exp->find<NodeButtonCustom>("tools-panels")) tick->on_click = [&app, popup_exp, main, tick](Node*)
|
||||
{
|
||||
const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::panels);
|
||||
if (menu_plan.action != pp::app::ToolsMenuAction::show_panels_submenu)
|
||||
return;
|
||||
|
||||
pp::panopainter::bind_legacy_tools_panels_submenu(app, *main, popup_exp, *tick);
|
||||
};
|
||||
|
||||
if (auto options = popup_exp->find<NodeButtonCustom>("tools-options")) options->on_click = [&app, options, main](Node* b)
|
||||
{
|
||||
const auto menu_plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::options);
|
||||
if (menu_plan.action != pp::app::ToolsMenuAction::show_options_submenu)
|
||||
return;
|
||||
|
||||
glm::vec2 pos = b->m_pos + glm::vec2(b->m_size.x, 0);
|
||||
auto popup_time = add_menu_popup(app, "options-menu", pos, b->m_size.x);
|
||||
if (!popup_time)
|
||||
return;
|
||||
|
||||
if (auto ui_scale = popup_time->find<NodeComboBox>("tools-ui-scale"))
|
||||
{
|
||||
std::vector<float> scale_options;
|
||||
scale_options.reserve(ui_scale->m_data.size());
|
||||
for (int i = 0; i < ui_scale->m_data.size(); i++)
|
||||
scale_options.push_back(ui_scale->get_float(i));
|
||||
|
||||
const auto selection = pp::app::plan_scale_option_selection(App::I->zoom, scale_options);
|
||||
if (selection.has_selection)
|
||||
ui_scale->set_index(static_cast<int>(selection.index));
|
||||
|
||||
ui_scale->on_select = [ui_scale](Node*, int index)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_ui_scale_preference(
|
||||
*App::I,
|
||||
ui_scale->get_float(index));
|
||||
if (!status.ok())
|
||||
LOG("UI scale preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
|
||||
if (auto vp_scale = popup_time->find<NodeComboBox>("tools-vp-scale"))
|
||||
{
|
||||
std::vector<float> scale_options;
|
||||
scale_options.reserve(vp_scale->m_data.size());
|
||||
for (int i = 0; i < vp_scale->m_data.size(); i++)
|
||||
scale_options.push_back(vp_scale->get_float(i));
|
||||
|
||||
const auto selection = pp::app::plan_scale_option_selection(App::I->canvas->m_density, scale_options);
|
||||
if (selection.has_selection)
|
||||
vp_scale->set_index(static_cast<int>(selection.index));
|
||||
|
||||
vp_scale->on_select = [vp_scale](Node*, int index)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_viewport_scale_preference(
|
||||
*App::I,
|
||||
vp_scale->get_float(index));
|
||||
if (!status.ok())
|
||||
LOG("Viewport scale preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
|
||||
if (auto rtl_btn = popup_time->find<NodeButtonCustom>("tools-rtl"))
|
||||
{
|
||||
NodeCheckBox* cb = rtl_btn->find<NodeCheckBox>("tools-rtl-check");
|
||||
cb->set_value(app.ui_rtl, false);
|
||||
|
||||
rtl_btn->on_click = [rtl_btn](Node*)
|
||||
{
|
||||
NodeCheckBox* cb = rtl_btn->find<NodeCheckBox>("tools-rtl-check");
|
||||
cb->set_value(!cb->checked, true);
|
||||
};
|
||||
|
||||
rtl_btn->find<NodeCheckBox>("tools-rtl-check")->on_value_changed = [&app](Node*, bool checked)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_interface_direction_preference(
|
||||
app,
|
||||
checked);
|
||||
if (!status.ok())
|
||||
LOG("Interface direction preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
|
||||
if (auto vr_btn = popup_time->find<NodeButtonCustom>("tools-vr"))
|
||||
{
|
||||
NodeCheckBox* cb = vr_btn->find<NodeCheckBox>("tools-vr-check");
|
||||
cb->set_value(app.vr_session_snapshot().has_vr);
|
||||
|
||||
vr_btn->on_click = [vr_btn](Node*)
|
||||
{
|
||||
NodeCheckBox* cb = vr_btn->find<NodeCheckBox>("tools-vr-check");
|
||||
cb->set_value(!cb->checked, true);
|
||||
};
|
||||
|
||||
vr_btn->find<NodeCheckBox>("tools-vr-check")->on_value_changed = [&app](Node* target, bool checked)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_vr_mode_preference(
|
||||
app,
|
||||
checked);
|
||||
if (!status.ok()) {
|
||||
auto cb = static_cast<NodeCheckBox*>(target);
|
||||
cb->set_value(false);
|
||||
app.message_box("VR Failed", "Couldn't start Virtual Reality mode");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (auto vr_btn = popup_time->find<NodeButtonCustom>("tools-vr-controllers"))
|
||||
{
|
||||
NodeCheckBox* cb = vr_btn->find<NodeCheckBox>("tools-vr-controllers-check");
|
||||
cb->set_value(app.vr_controllers_enabled);
|
||||
|
||||
vr_btn->on_click = [vr_btn](Node*)
|
||||
{
|
||||
NodeCheckBox* cb = vr_btn->find<NodeCheckBox>("tools-vr-controllers-check");
|
||||
cb->set_value(!cb->checked, true);
|
||||
};
|
||||
|
||||
vr_btn->find<NodeCheckBox>("tools-vr-controllers-check")->on_value_changed = [&app](Node*, bool checked)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_vr_controllers_preference(
|
||||
app,
|
||||
checked);
|
||||
if (!status.ok())
|
||||
LOG("VR controllers preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
|
||||
if (auto btn = popup_time->find<NodeButtonCustom>("tools-timelapse"))
|
||||
{
|
||||
NodeCheckBox* cb = btn->find<NodeCheckBox>("tools-timelapse-check");
|
||||
cb->set_value(
|
||||
pp::panopainter::read_legacy_startup_preferences(app.vr_controllers_enabled).auto_timelapse,
|
||||
false);
|
||||
|
||||
btn->on_click = [btn](Node*)
|
||||
{
|
||||
NodeCheckBox* cb = btn->find<NodeCheckBox>("tools-timelapse-check");
|
||||
cb->set_value(!cb->checked, true);
|
||||
};
|
||||
|
||||
btn->find<NodeCheckBox>("tools-timelapse-check")->on_value_changed = [&app](Node*, bool checked)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_timelapse_preference(
|
||||
app,
|
||||
checked);
|
||||
if (!status.ok())
|
||||
LOG("Timelapse preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
|
||||
if (auto mode = popup_time->find<NodeComboBox>("tools-show-cursor"))
|
||||
{
|
||||
mode->set_index(pp::panopainter::read_legacy_canvas_preferences().cursor_mode);
|
||||
|
||||
mode->on_select = [](Node*, int index)
|
||||
{
|
||||
const auto status = pp::panopainter::execute_legacy_canvas_cursor_mode_preference(
|
||||
*App::I,
|
||||
index);
|
||||
if (!status.ok())
|
||||
LOG("Cursor mode preference failed: %s", status.message);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
popup_exp->find<NodeButtonCustom>("clear-grids")->on_click = [&app, popup_exp](Node*) {
|
||||
auto popup_exp_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_exp->root(), popup_exp);
|
||||
if (!popup_exp_overlay)
|
||||
return;
|
||||
const auto popup_exp_handle = popup_exp_overlay.value();
|
||||
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::clear_grids);
|
||||
pp::panopainter::execute_legacy_tools_menu_plan(app, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_exp->root(), popup_exp_handle);
|
||||
}
|
||||
};
|
||||
|
||||
popup_exp->find<NodeButtonCustom>("camera-reset")->on_click = [&app, popup_exp](Node*) {
|
||||
auto popup_exp_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_exp->root(), popup_exp);
|
||||
if (!popup_exp_overlay)
|
||||
return;
|
||||
const auto popup_exp_handle = popup_exp_overlay.value();
|
||||
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::reset_camera);
|
||||
pp::panopainter::execute_legacy_tools_menu_plan(app, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_exp->root(), popup_exp_handle);
|
||||
}
|
||||
};
|
||||
|
||||
popup_exp->find<NodeButtonCustom>("shortcuts")->on_click = [&app, popup_exp](Node*) {
|
||||
auto popup_exp_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_exp->root(), popup_exp);
|
||||
if (!popup_exp_overlay)
|
||||
return;
|
||||
const auto popup_exp_handle = popup_exp_overlay.value();
|
||||
const auto plan = pp::app::plan_tools_menu_command(pp::app::ToolsMenuCommand::shortcuts);
|
||||
pp::panopainter::execute_legacy_tools_menu_plan(app, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_exp->root(), popup_exp_handle);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
popup_exp->find<NodeButtonCustom>("mp4test")->on_click = [this, popup_exp](Node*) {
|
||||
dialog_export_mp4();
|
||||
pp::panopainter::close_legacy_popup_overlay(*popup_exp);
|
||||
};
|
||||
*/
|
||||
|
||||
if (app.platform_supports_sonarpen())
|
||||
{
|
||||
popup_exp->find<NodeButtonCustom>("sonarpen")->on_click = [&app, popup_exp](Node*) {
|
||||
auto popup_exp_overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*popup_exp->root(), popup_exp);
|
||||
if (!popup_exp_overlay)
|
||||
return;
|
||||
const auto popup_exp_handle = popup_exp_overlay.value();
|
||||
const auto plan = pp::app::plan_tools_menu_command(
|
||||
pp::app::ToolsMenuCommand::sonarpen,
|
||||
app.platform_supports_sonarpen());
|
||||
pp::panopainter::execute_legacy_tools_menu_plan(app, plan);
|
||||
if (plan.closes_root_popup)
|
||||
{
|
||||
close_legacy_overlay_handle_ignoring_status(*popup_exp->root(), popup_exp_handle);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pp::panopainter
|
||||
49
src/app_layout_ui_state.cpp
Normal file
49
src/app_layout_ui_state.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/app_preferences.h"
|
||||
#include "legacy_app_ui_state_services.h"
|
||||
#include "legacy_preference_storage.h"
|
||||
|
||||
void App::set_ui_rtl(bool rtl)
|
||||
{
|
||||
const auto plan = pp::app::plan_interface_direction(rtl);
|
||||
ui_rtl = plan.direction == pp::app::InterfaceDirection::right_to_left;
|
||||
layout[main_id]->find("central-row")->SetRTL(
|
||||
ui_rtl ? YGDirectionRTL : YGDirectionLTR);
|
||||
}
|
||||
|
||||
bool App::get_ui_rtl() const
|
||||
{
|
||||
return ui_rtl;
|
||||
}
|
||||
|
||||
void App::ui_save()
|
||||
{
|
||||
Serializer::Descriptor d;
|
||||
pp::panopainter::save_legacy_ui_panel_state(
|
||||
d,
|
||||
layout[main_id]->find_ref("floatings"),
|
||||
layout[main_id]->find_ref("drop-left"),
|
||||
layout[main_id]->find_ref("drop-right"));
|
||||
pp::panopainter::set_legacy_ui_state_preferences(d, ui_rtl);
|
||||
save_platform_ui_state();
|
||||
|
||||
pp::panopainter::save_legacy_preferences();
|
||||
}
|
||||
|
||||
void App::ui_restore()
|
||||
{
|
||||
const auto preferences = pp::panopainter::read_legacy_ui_preferences();
|
||||
if (preferences.has_rtl)
|
||||
set_ui_rtl(preferences.rtl);
|
||||
|
||||
if (!preferences.state)
|
||||
return;
|
||||
|
||||
auto floatings = layout[main_id]->find_ref("floatings");
|
||||
auto drop_left = layout[main_id]->find_ref("drop-left");
|
||||
auto drop_right = layout[main_id]->find_ref("drop-right");
|
||||
auto d = preferences.state;
|
||||
|
||||
pp::panopainter::restore_legacy_ui_panel_state(*this, floatings, drop_left, drop_right, *d);
|
||||
}
|
||||
643
src/app_runtime.cpp
Normal file
643
src/app_runtime.cpp
Normal file
@@ -0,0 +1,643 @@
|
||||
#include "pch.h"
|
||||
#include "app_runtime.h"
|
||||
|
||||
#include "app.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void execute_render_worker_task(AppTask& task) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LOG("render worker task failed");
|
||||
}
|
||||
}
|
||||
|
||||
void execute_ui_worker_task(AppTask& task) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LOG("ui worker task failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AppRuntime::AppRuntime()
|
||||
: prepared_file_worker_([this](std::stop_token stop_token)
|
||||
{
|
||||
prepared_file_worker_main(stop_token);
|
||||
})
|
||||
, canvas_async_worker_([this](std::stop_token stop_token)
|
||||
{
|
||||
canvas_async_worker_main(stop_token);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
AppRuntime::~AppRuntime()
|
||||
{
|
||||
canvas_async_worker_stop();
|
||||
prepared_file_worker_stop();
|
||||
}
|
||||
|
||||
bool AppRuntime::is_render_thread() const noexcept
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
return std::this_thread::get_id() == render_thread_id_;
|
||||
}
|
||||
|
||||
bool AppRuntime::is_ui_thread() const noexcept
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
return std::this_thread::get_id() == ui_thread_id_;
|
||||
}
|
||||
|
||||
void AppRuntime::notify_render_worker() noexcept
|
||||
{
|
||||
render_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AppRuntime::notify_ui_worker() noexcept
|
||||
{
|
||||
ui_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AppRuntime::prepared_file_task(std::function<void()> task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(prepared_file_task_mutex_);
|
||||
if (!prepared_file_running_)
|
||||
return;
|
||||
prepared_file_tasklist_.push_back(std::move(task));
|
||||
}
|
||||
prepared_file_cv_.notify_one();
|
||||
}
|
||||
|
||||
void AppRuntime::canvas_async_task(std::function<void()> task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(canvas_async_task_mutex_);
|
||||
if (!canvas_async_running_)
|
||||
return;
|
||||
canvas_async_tasklist_.push_back(std::move(task));
|
||||
}
|
||||
canvas_async_cv_.notify_one();
|
||||
}
|
||||
|
||||
void AppRuntime::main_thread_task(std::packaged_task<void()> task)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(main_thread_task_mutex_);
|
||||
main_thread_tasklist_.push_back(std::move(task));
|
||||
}
|
||||
|
||||
void AppRuntime::drain_main_thread_tasks()
|
||||
{
|
||||
std::deque<std::packaged_task<void()>> tasklist;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(main_thread_task_mutex_);
|
||||
tasklist = std::move(main_thread_tasklist_);
|
||||
}
|
||||
|
||||
while (!tasklist.empty())
|
||||
{
|
||||
tasklist.front()();
|
||||
tasklist.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
std::future<void> AppRuntime::render_task_async(AppTask task, bool unique)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
const auto on_render_thread = is_render_thread();
|
||||
const auto running = render_running_.load(std::memory_order_acquire);
|
||||
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
on_render_thread,
|
||||
unique,
|
||||
0U,
|
||||
running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
execute_render_worker_task(task);
|
||||
return future;
|
||||
}
|
||||
|
||||
if (!dispatch.queue_task)
|
||||
return future;
|
||||
|
||||
bool notify_worker = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
if (!render_running_.load(std::memory_order_acquire))
|
||||
return future;
|
||||
|
||||
const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
render_tasklist_.size(),
|
||||
true,
|
||||
false,
|
||||
false);
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
{
|
||||
render_tasklist_.erase(
|
||||
std::remove_if(render_tasklist_.begin(), render_tasklist_.end(),
|
||||
[id = task.task_id](AppTask const& t) { return t.task_id == id; }),
|
||||
render_tasklist_.end());
|
||||
}
|
||||
render_tasklist_.push_back(std::move(task));
|
||||
notify_worker = queue_dispatch.notify_worker;
|
||||
}
|
||||
if (notify_worker)
|
||||
render_cv_.notify_all();
|
||||
return future;
|
||||
}
|
||||
|
||||
void AppRuntime::render_task(AppTask task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
const auto on_render_thread = is_render_thread();
|
||||
const auto running = render_running_.load(std::memory_order_acquire);
|
||||
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
on_render_thread,
|
||||
false,
|
||||
0U,
|
||||
running,
|
||||
true,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
execute_render_worker_task(task);
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
bool notify_worker = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
if (!render_running_.load(std::memory_order_acquire))
|
||||
return;
|
||||
render_tasklist_.push_back(std::move(task));
|
||||
notify_worker = dispatch.notify_worker;
|
||||
}
|
||||
if (notify_worker)
|
||||
render_cv_.notify_all();
|
||||
}
|
||||
|
||||
if (dispatch.wait_for_completion)
|
||||
future.get();
|
||||
}
|
||||
|
||||
std::future<void> AppRuntime::ui_task_async(AppTask task, bool unique)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
const auto on_ui_thread = is_ui_thread();
|
||||
const auto running = ui_running_.load(std::memory_order_acquire);
|
||||
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
on_ui_thread,
|
||||
unique,
|
||||
0U,
|
||||
running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
execute_ui_worker_task(task);
|
||||
return future;
|
||||
}
|
||||
|
||||
if (!dispatch.queue_task)
|
||||
return future;
|
||||
|
||||
bool notify_worker = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
if (!ui_running_.load(std::memory_order_acquire))
|
||||
return future;
|
||||
|
||||
const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
ui_tasklist_.size(),
|
||||
true,
|
||||
false,
|
||||
false);
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
{
|
||||
ui_tasklist_.erase(
|
||||
std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(),
|
||||
[id = task.task_id](AppTask const& t) { return t.task_id == id; }),
|
||||
ui_tasklist_.end());
|
||||
}
|
||||
ui_tasklist_.push_back(std::move(task));
|
||||
notify_worker = queue_dispatch.notify_worker;
|
||||
}
|
||||
if (notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
return future;
|
||||
}
|
||||
|
||||
void AppRuntime::ui_task(AppTask task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
const auto on_ui_thread = is_ui_thread();
|
||||
const auto running = ui_running_.load(std::memory_order_acquire);
|
||||
const auto dispatch = pp::app::plan_app_runtime_task_dispatch(
|
||||
on_ui_thread,
|
||||
false,
|
||||
0U,
|
||||
running,
|
||||
true,
|
||||
true);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
execute_ui_worker_task(task);
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
bool notify_worker = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
if (!ui_running_.load(std::memory_order_acquire))
|
||||
return;
|
||||
ui_tasklist_.push_back(std::move(task));
|
||||
notify_worker = dispatch.notify_worker;
|
||||
}
|
||||
if (notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
}
|
||||
|
||||
if (dispatch.wait_for_completion)
|
||||
future.get();
|
||||
if (dispatch.request_redraw)
|
||||
request_redraw_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AppRuntime::prepared_file_worker_main(std::stop_token stop_token)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(prepared_file_task_mutex_);
|
||||
prepared_file_cv_.wait(lock, [this, &stop_token]
|
||||
{
|
||||
return stop_token.stop_requested() || !prepared_file_running_ || !prepared_file_tasklist_.empty();
|
||||
});
|
||||
if ((stop_token.stop_requested() || !prepared_file_running_) && prepared_file_tasklist_.empty())
|
||||
break;
|
||||
task = std::move(prepared_file_tasklist_.front());
|
||||
prepared_file_tasklist_.pop_front();
|
||||
}
|
||||
|
||||
if (task)
|
||||
{
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LOG("prepared file worker task failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::canvas_async_worker_main(std::stop_token stop_token)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(canvas_async_task_mutex_);
|
||||
canvas_async_cv_.wait(lock, [this, &stop_token]
|
||||
{
|
||||
return stop_token.stop_requested() || !canvas_async_running_ || !canvas_async_tasklist_.empty();
|
||||
});
|
||||
if ((stop_token.stop_requested() || !canvas_async_running_) && canvas_async_tasklist_.empty())
|
||||
break;
|
||||
task = std::move(canvas_async_tasklist_.front());
|
||||
canvas_async_tasklist_.pop_front();
|
||||
}
|
||||
|
||||
if (task)
|
||||
{
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LOG("canvas async worker task failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::prepared_file_worker_stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(prepared_file_task_mutex_);
|
||||
prepared_file_running_ = false;
|
||||
}
|
||||
prepared_file_cv_.notify_all();
|
||||
if (prepared_file_worker_.joinable())
|
||||
{
|
||||
prepared_file_worker_.request_stop();
|
||||
prepared_file_worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::canvas_async_worker_stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(canvas_async_task_mutex_);
|
||||
canvas_async_running_ = false;
|
||||
}
|
||||
canvas_async_cv_.notify_all();
|
||||
if (canvas_async_worker_.joinable())
|
||||
{
|
||||
canvas_async_worker_.request_stop();
|
||||
canvas_async_worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_tick(App& app)
|
||||
{
|
||||
static uint32_t count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
render_thread_id_ = std::this_thread::get_id();
|
||||
}
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppQueueDrainPlan drain_plan;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex_);
|
||||
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
|
||||
render_running_.store(drain_plan.mark_running, std::memory_order_release);
|
||||
if (!drain_plan.drain_tasks)
|
||||
return;
|
||||
working_list = std::move(render_tasklist_);
|
||||
}
|
||||
|
||||
if (drain_plan.wrap_in_render_context)
|
||||
{
|
||||
app.async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
count++;
|
||||
execute_render_worker_task(working_list.front());
|
||||
working_list.pop_front();
|
||||
}
|
||||
app.async_end();
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_main(App& app, std::stop_token stop_token)
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
render_thread_id_ = std::this_thread::get_id();
|
||||
}
|
||||
render_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
|
||||
for (;;)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex_);
|
||||
render_cv_.wait(lock, [this, &stop_token]
|
||||
{
|
||||
return stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire) || !render_tasklist_.empty();
|
||||
});
|
||||
if (render_tasklist_.empty())
|
||||
{
|
||||
if (stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire))
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
working_list = std::move(render_tasklist_);
|
||||
}
|
||||
|
||||
app.async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
execute_render_worker_task(working_list.front());
|
||||
working_list.pop_front();
|
||||
}
|
||||
app.async_end();
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_tick(App& app)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
ui_thread_id_ = std::this_thread::get_id();
|
||||
}
|
||||
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppUiTickPlan tick_plan;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex_);
|
||||
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw);
|
||||
ui_running_.store(tick_plan.mark_running, std::memory_order_release);
|
||||
working_list = std::move(ui_tasklist_);
|
||||
}
|
||||
|
||||
if (tick_plan.execute_tasks)
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
execute_ui_worker_task(working_list.front());
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if (tick_plan.tick_app)
|
||||
app.tick(0);
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, 0);
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
app.update(0);
|
||||
app.render_task([&app]
|
||||
{
|
||||
app.bind_default_render_target();
|
||||
app.clear();
|
||||
app.draw(0);
|
||||
app.async_swap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token)
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
ui_thread_id_ = std::this_thread::get_id();
|
||||
}
|
||||
ui_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release);
|
||||
|
||||
app.attach_ui_thread();
|
||||
|
||||
LOG("ui thread init()");
|
||||
app.init();
|
||||
|
||||
auto t_start = std::chrono::high_resolution_clock::now();
|
||||
float t_frame = 0;
|
||||
float t_fps_counter = 0;
|
||||
float t_reloader = 0;
|
||||
int rendered_frames = 0;
|
||||
for (;;)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex_);
|
||||
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token]
|
||||
{
|
||||
return stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire) || !ui_tasklist_.empty();
|
||||
});
|
||||
if (ui_tasklist_.empty())
|
||||
{
|
||||
if (stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire))
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
working_list = std::move(ui_tasklist_);
|
||||
}
|
||||
}
|
||||
|
||||
if (!working_list.empty())
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
execute_ui_worker_task(working_list.front());
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
auto t_now = std::chrono::high_resolution_clock::now();
|
||||
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
||||
t_start = t_now;
|
||||
|
||||
const auto timer_plan = pp::app::plan_app_ui_loop_timers(
|
||||
dt,
|
||||
t_frame,
|
||||
t_fps_counter,
|
||||
t_reloader,
|
||||
rendered_frames,
|
||||
app.platform_enables_live_asset_reloading());
|
||||
if (timer_plan) {
|
||||
if (timer_plan.value().update_platform_frame)
|
||||
app.update_platform_frame(dt);
|
||||
t_frame = timer_plan.value().frame_accumulator;
|
||||
t_fps_counter = timer_plan.value().fps_accumulator;
|
||||
t_reloader = timer_plan.value().reload_accumulator;
|
||||
rendered_frames = timer_plan.value().rendered_frames_after_report;
|
||||
|
||||
if (timer_plan.value().report_rendered_frames)
|
||||
app.report_rendered_frames(timer_plan.value().reported_frame_count);
|
||||
|
||||
if (timer_plan.value().check_live_asset_reload) {
|
||||
if (ShaderManager::reload())
|
||||
{
|
||||
app.stroke->update_controls();
|
||||
app.redraw = true;
|
||||
}
|
||||
if (app.layout.reload())
|
||||
app.redraw = true;
|
||||
if (app.layout_designer.reload())
|
||||
app.redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, rendered_frames);
|
||||
if (redraw_plan.tick_app)
|
||||
app.tick(dt);
|
||||
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
app.update(t_frame);
|
||||
app.render_task([&app, t_frame]
|
||||
{
|
||||
app.bind_default_render_target();
|
||||
app.clear();
|
||||
app.draw(t_frame);
|
||||
app.async_swap();
|
||||
});
|
||||
if (redraw_plan.reset_frame_accumulator)
|
||||
t_frame = 0;
|
||||
rendered_frames = redraw_plan.rendered_frames;
|
||||
}
|
||||
}
|
||||
app.detach_ui_thread();
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_start(App& app)
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
render_thread_ = std::jthread([this, &app](std::stop_token stop_token)
|
||||
{
|
||||
render_thread_main(app, stop_token);
|
||||
});
|
||||
render_running_.store(plan.mark_running, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
|
||||
if (plan.mark_not_running)
|
||||
render_running_.store(false, std::memory_order_release);
|
||||
if (plan.join_thread)
|
||||
render_thread_.request_stop();
|
||||
if (plan.notify_worker)
|
||||
render_cv_.notify_all();
|
||||
if (plan.join_thread)
|
||||
render_thread_.join();
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_start(App& app)
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
ui_thread_ = std::jthread([this, &app](std::stop_token stop_token)
|
||||
{
|
||||
ui_thread_main(app, stop_token);
|
||||
});
|
||||
ui_running_.store(plan.mark_running, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
|
||||
if (plan.mark_not_running)
|
||||
ui_running_.store(false, std::memory_order_release);
|
||||
if (plan.join_thread)
|
||||
ui_thread_.request_stop();
|
||||
if (plan.notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
if (plan.join_thread)
|
||||
ui_thread_.join();
|
||||
}
|
||||
137
src/app_runtime.h
Normal file
137
src/app_runtime.h
Normal file
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_thread.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
#include <thread>
|
||||
|
||||
class App;
|
||||
|
||||
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 AppRuntime
|
||||
{
|
||||
public:
|
||||
AppRuntime();
|
||||
~AppRuntime();
|
||||
|
||||
[[nodiscard]] bool is_render_thread() const noexcept;
|
||||
[[nodiscard]] bool is_ui_thread() const noexcept;
|
||||
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_.load(std::memory_order_acquire); }
|
||||
void clear_request_redraw() noexcept { request_redraw_.store(false, std::memory_order_release); }
|
||||
|
||||
void notify_render_worker() noexcept;
|
||||
void notify_ui_worker() noexcept;
|
||||
void prepared_file_task(std::function<void()> task);
|
||||
void canvas_async_task(std::function<void()> task);
|
||||
void main_thread_task(std::packaged_task<void()> task);
|
||||
void drain_main_thread_tasks();
|
||||
|
||||
std::future<void> render_task_async(AppTask task, bool unique = false);
|
||||
void render_task(AppTask task);
|
||||
std::future<void> ui_task_async(AppTask task, bool unique = false);
|
||||
void ui_task(AppTask task);
|
||||
|
||||
void render_thread_tick(App& app);
|
||||
void render_thread_main(App& app, std::stop_token stop_token);
|
||||
void render_thread_start(App& app);
|
||||
void render_thread_stop();
|
||||
|
||||
void ui_thread_tick(App& app);
|
||||
void ui_thread_main(App& app, std::stop_token stop_token);
|
||||
void ui_thread_start(App& app);
|
||||
void ui_thread_stop();
|
||||
|
||||
template<typename T>
|
||||
std::future<void> render_task_async(T task, bool unique = false)
|
||||
{
|
||||
return render_task_async(AppTask(std::move(task)), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void render_task(T task)
|
||||
{
|
||||
render_task(AppTask(std::move(task)));
|
||||
}
|
||||
|
||||
void render_sync()
|
||||
{
|
||||
render_task([] {});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::future<void> ui_task_async(T task, bool unique = false)
|
||||
{
|
||||
return ui_task_async(AppTask(std::move(task)), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void ui_task(T task)
|
||||
{
|
||||
ui_task(AppTask(std::move(task)));
|
||||
}
|
||||
|
||||
void ui_sync()
|
||||
{
|
||||
ui_task([] {});
|
||||
}
|
||||
|
||||
private:
|
||||
void prepared_file_worker_main(std::stop_token stop_token);
|
||||
void prepared_file_worker_stop();
|
||||
void canvas_async_worker_main(std::stop_token stop_token);
|
||||
void canvas_async_worker_stop();
|
||||
|
||||
std::deque<std::function<void()>> prepared_file_tasklist_;
|
||||
std::mutex prepared_file_task_mutex_;
|
||||
std::condition_variable prepared_file_cv_;
|
||||
std::jthread prepared_file_worker_;
|
||||
bool prepared_file_running_ = true;
|
||||
|
||||
std::deque<std::function<void()>> canvas_async_tasklist_;
|
||||
std::mutex canvas_async_task_mutex_;
|
||||
std::condition_variable canvas_async_cv_;
|
||||
std::jthread canvas_async_worker_;
|
||||
bool canvas_async_running_ = true;
|
||||
|
||||
std::deque<std::packaged_task<void()>> main_thread_tasklist_;
|
||||
std::mutex main_thread_task_mutex_;
|
||||
|
||||
std::deque<AppTask> render_tasklist_;
|
||||
mutable std::mutex render_task_mutex_;
|
||||
std::condition_variable render_cv_;
|
||||
std::jthread render_thread_;
|
||||
std::thread::id render_thread_id_;
|
||||
std::atomic_bool render_running_ = false;
|
||||
|
||||
std::deque<AppTask> ui_tasklist_;
|
||||
mutable std::mutex ui_task_mutex_;
|
||||
std::condition_variable ui_cv_;
|
||||
std::jthread ui_thread_;
|
||||
std::thread::id ui_thread_id_;
|
||||
std::atomic_bool ui_running_ = false;
|
||||
std::atomic_bool request_redraw_ = false;
|
||||
};
|
||||
@@ -1,30 +1,20 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_gl_runtime_dispatch.h"
|
||||
#include "renderer_api/shader_catalog.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] GLenum extension_count_query() noexcept
|
||||
void apply_shader_manager_feature_state(pp::renderer::gl::OpenGlFeatureState feature_state) noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::extension_count_query());
|
||||
}
|
||||
|
||||
[[nodiscard]] GLenum extension_string_name() noexcept
|
||||
{
|
||||
return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
|
||||
{
|
||||
pp::renderer::gl::OpenGlCapabilities capabilities;
|
||||
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
|
||||
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
|
||||
capabilities.float32_textures = ShaderManager::ext_float32;
|
||||
capabilities.float32_linear = ShaderManager::ext_float32_linear;
|
||||
capabilities.float16_textures = ShaderManager::ext_float16;
|
||||
return capabilities;
|
||||
ShaderManager::ext_framebuffer_fetch = feature_state.capabilities.framebuffer_fetch;
|
||||
ShaderManager::ext_map_aligned = feature_state.capabilities.map_buffer_alignment;
|
||||
ShaderManager::ext_float32 = feature_state.capabilities.float32_textures;
|
||||
ShaderManager::ext_float32_linear = feature_state.capabilities.float32_linear;
|
||||
ShaderManager::ext_float16 = feature_state.capabilities.float16_textures;
|
||||
ShaderManager::set_render_device_features(feature_state.features);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,46 +27,25 @@ void App::initShaders()
|
||||
#endif // _DEBUG
|
||||
|
||||
render_task([] {
|
||||
GLint n_exts;
|
||||
glGetIntegerv(extension_count_query(), &n_exts);
|
||||
std::vector<std::string> extension_storage;
|
||||
std::vector<std::string_view> extension_views;
|
||||
extension_storage.reserve(n_exts);
|
||||
extension_views.reserve(n_exts);
|
||||
for (int i = 0; i < n_exts; i++)
|
||||
{
|
||||
extension_storage.emplace_back((const char*)glGetStringi(extension_string_name(), i));
|
||||
extension_views.push_back(extension_storage.back());
|
||||
LOG("EXT: %s", extension_storage.back().c_str());
|
||||
const auto detection_result = pp::renderer::gl::query_opengl_capability_detection(
|
||||
pp::legacy::gl_runtime::extension_query_dispatch(),
|
||||
pp::renderer::gl::opengl_runtime_for_current_build());
|
||||
if (!detection_result.ok()) {
|
||||
LOG("OpenGL capability detection failed: %s", detection_result.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
pp::renderer::gl::OpenGlRuntime runtime;
|
||||
#if __GL__
|
||||
runtime.desktop_gl = true;
|
||||
#endif
|
||||
#if __GLES__
|
||||
runtime.gles = true;
|
||||
#endif
|
||||
#if __WEB__
|
||||
runtime.web = true;
|
||||
#endif
|
||||
const auto capabilities = pp::renderer::gl::detect_opengl_capabilities(extension_views, runtime);
|
||||
ShaderManager::ext_framebuffer_fetch = capabilities.framebuffer_fetch;
|
||||
ShaderManager::ext_map_aligned = capabilities.map_buffer_alignment;
|
||||
ShaderManager::ext_float32 = capabilities.float32_textures;
|
||||
ShaderManager::ext_float32_linear = capabilities.float32_linear;
|
||||
ShaderManager::ext_float16 = capabilities.float16_textures;
|
||||
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
|
||||
const auto& detection = detection_result.value();
|
||||
for (const auto& extension : detection.extensions) {
|
||||
LOG("EXT: %s", extension.c_str());
|
||||
}
|
||||
|
||||
apply_shader_manager_feature_state(detection.feature_state);
|
||||
});
|
||||
|
||||
#if __GL__
|
||||
// In OpenGL 3.3 these should be already available
|
||||
ShaderManager::ext_float32_linear = true;
|
||||
ShaderManager::ext_float32 = true;
|
||||
ShaderManager::ext_float16 = true;
|
||||
#endif
|
||||
ShaderManager::set_render_device_features(
|
||||
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
|
||||
apply_shader_manager_feature_state(pp::renderer::gl::detect_opengl_feature_state(
|
||||
std::span<const std::string_view> {},
|
||||
pp::renderer::gl::opengl_runtime_for_current_build()));
|
||||
|
||||
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
|
||||
|
||||
|
||||
239
src/app_vr.cpp
239
src/app_vr.cpp
@@ -3,45 +3,26 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "app.h"
|
||||
#include "legacy_canvas_draw_merge_services.h"
|
||||
#include "legacy_canvas_stroke_composite_services.h"
|
||||
#include "legacy_canvas_stroke_erase_services.h"
|
||||
#include "legacy_canvas_stroke_preview_services.h"
|
||||
#include "legacy_ui_gl_dispatch.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "util.h"
|
||||
#include "shape.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
bool win32_vr_start();
|
||||
void win32_vr_stop();
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
void set_active_texture_unit(std::uint32_t unit_index)
|
||||
{
|
||||
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
|
||||
pp::legacy::ui_gl::activate_texture_unit(unit_index, "OpenGL VR");
|
||||
}
|
||||
|
||||
void unbind_texture_2d()
|
||||
{
|
||||
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
|
||||
}
|
||||
|
||||
void enable_opengl_state(std::uint32_t state) noexcept
|
||||
{
|
||||
glEnable(static_cast<GLenum>(state));
|
||||
}
|
||||
|
||||
void disable_opengl_state(std::uint32_t state) noexcept
|
||||
{
|
||||
glDisable(static_cast<GLenum>(state));
|
||||
}
|
||||
|
||||
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
|
||||
{
|
||||
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||
}
|
||||
|
||||
void clear_opengl_mask(std::uint32_t mask) noexcept
|
||||
{
|
||||
glClear(static_cast<GLbitfield>(mask));
|
||||
pp::legacy::ui_gl::unbind_texture_2d("OpenGL VR");
|
||||
}
|
||||
|
||||
void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
|
||||
@@ -49,7 +30,7 @@ void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
|
||||
const auto status = pp::renderer::gl::apply_opengl_viewport(
|
||||
viewport,
|
||||
pp::renderer::gl::OpenGlViewportDispatch {
|
||||
.viewport = set_opengl_viewport,
|
||||
.viewport = pp::legacy::ui_gl::set_opengl_viewport,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR UI viewport failed: %s", status.message);
|
||||
@@ -60,8 +41,8 @@ void apply_vr_ui_scissor_test(bool enabled)
|
||||
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
|
||||
enabled,
|
||||
pp::renderer::gl::OpenGlScissorTestDispatch {
|
||||
.enable = enable_opengl_state,
|
||||
.disable = disable_opengl_state,
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR UI scissor test failed: %s", status.message);
|
||||
@@ -73,19 +54,33 @@ void apply_vr_render_capability(std::uint32_t state, bool enabled)
|
||||
state,
|
||||
enabled,
|
||||
pp::renderer::gl::OpenGlCapabilityDispatch {
|
||||
.enable = enable_opengl_state,
|
||||
.disable = disable_opengl_state,
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR render state failed: %s", status.message);
|
||||
}
|
||||
|
||||
bool query_vr_render_capability(std::uint32_t state)
|
||||
{
|
||||
const auto result = pp::renderer::gl::query_opengl_capability_state(
|
||||
state,
|
||||
pp::renderer::gl::OpenGlCapabilityStateQueryDispatch {
|
||||
.is_enabled = pp::legacy::ui_gl::is_opengl_state_enabled,
|
||||
});
|
||||
if (!result.ok()) {
|
||||
LOG("OpenGL VR render state query failed: %s", result.status().message);
|
||||
return false;
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
void clear_vr_depth_buffer()
|
||||
{
|
||||
const auto status = pp::renderer::gl::clear_opengl_buffers(
|
||||
pp::renderer::gl::framebuffer_depth_buffer_mask(),
|
||||
pp::renderer::gl::OpenGlBufferClearDispatch {
|
||||
.clear = clear_opengl_mask,
|
||||
.clear = pp::legacy::ui_gl::clear_opengl_buffer,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR depth clear failed: %s", status.message);
|
||||
@@ -104,18 +99,12 @@ Sphere controller_ray;
|
||||
|
||||
bool App::vr_start()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return win32_vr_start();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
return start_platform_vr_mode();
|
||||
}
|
||||
|
||||
void App::vr_stop()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_vr_stop();
|
||||
#endif
|
||||
stop_platform_vr_mode();
|
||||
}
|
||||
|
||||
void App::vr_draw_ui()
|
||||
@@ -139,6 +128,8 @@ void App::vr_update(float dt)
|
||||
{
|
||||
if (!vr_controllers_enabled)
|
||||
return;
|
||||
|
||||
const auto vr_session = vr_session_snapshot();
|
||||
canvas->m_canvas->m_cam_fov = 60;
|
||||
float tan_fov = glm::tan(glm::radians(canvas->m_canvas->m_cam_fov / 2.f));
|
||||
glm::vec3 aspect = { (float)uirtt.getWidth() / (float)uirtt.getHeight(), 1.f, 1.f };
|
||||
@@ -153,8 +144,8 @@ void App::vr_update(float dt)
|
||||
auto r = glm::normalize(glm::vec3(glm::vec4(1, 0, 0, 0) * mm));
|
||||
auto n = glm::normalize(glm::vec3(glm::vec4(0, 0, 1, 0) * mm));
|
||||
auto u = glm::normalize(glm::vec3(glm::vec4(0, 1, 0, 0) * mm));
|
||||
auto co = vr_controllers[0].get_pos();
|
||||
auto cd = glm::mat3(vr_controllers[0].m_mat) * glm::mat3(glm::eulerAngleX(glm::radians(-30.f))) * glm::vec3(0, 0, -1);
|
||||
auto co = vr_session.vr_controllers[0].get_pos();
|
||||
auto cd = glm::mat3(vr_session.vr_controllers[0].m_mat) * glm::mat3(glm::eulerAngleX(glm::radians(-30.f))) * glm::vec3(0, 0, -1);
|
||||
ui_inside = false;
|
||||
glm::vec3 hit;
|
||||
float t;
|
||||
@@ -178,7 +169,7 @@ void App::vr_update(float dt)
|
||||
|
||||
if (down_controller)
|
||||
{
|
||||
glm::vec3 head_position = vr_head[3];
|
||||
glm::vec3 head_position = vr_session.vr_head[3];
|
||||
glm::vec3 c_pos = glm::normalize(down_controller->get_pos() - head_position) * 800.f;
|
||||
controller_points.add(c_pos);
|
||||
auto p = controller_points.average();
|
||||
@@ -221,7 +212,7 @@ void App::vr_analog(const VRController& c, VRController::kButton b, VRController
|
||||
{
|
||||
if (a == VRController::kAction::Press)
|
||||
{
|
||||
glm::vec3 head_position = vr_head[3];
|
||||
glm::vec3 head_position = vr_session_snapshot().vr_head[3];
|
||||
glm::vec3 c_pos = glm::normalize(c.get_pos() - head_position) * 800.f;
|
||||
render_task_async([=] {
|
||||
Canvas::I->stroke_start(c_pos, force.x);
|
||||
@@ -273,8 +264,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::vec3 origin = glm::vec3(0, 0, -1) * glm::transpose(glm::mat3(pose));
|
||||
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
|
||||
|
||||
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
|
||||
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
|
||||
const bool blend = query_vr_render_capability(pp::renderer::gl::blend_state());
|
||||
const bool depth = query_vr_render_capability(pp::renderer::gl::depth_test_state());
|
||||
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||
@@ -287,9 +278,11 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
canvas->m_canvas->m_plane_transform[plane_index] *
|
||||
glm::translate(glm::vec3(0, 0, -1));
|
||||
|
||||
ShaderManager::use(kShader::Checkerboard);
|
||||
ShaderManager::u_int(kShaderUniform::Colorize, false);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(
|
||||
pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms {
|
||||
.mvp = plane_mvp,
|
||||
.colorize = false,
|
||||
});
|
||||
m_face_plane.draw_fill();
|
||||
}
|
||||
|
||||
@@ -321,14 +314,16 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
sampler.bind(1);
|
||||
sampler.bind(2);
|
||||
|
||||
ShaderManager::use(kShader::CompErase);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_int(kShaderUniform::TexStroke, 1);
|
||||
ShaderManager::u_int(kShaderUniform::TexMask, 2);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, canvas->m_canvas->m_layers[layer_index]->m_opacity);
|
||||
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
|
||||
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
pp::panopainter::setup_legacy_stroke_erase_shader(
|
||||
pp::panopainter::LegacyStrokeEraseUniforms {
|
||||
.mvp = plane_mvp_z,
|
||||
.texture_slot = 0,
|
||||
.stroke_texture_slot = 1,
|
||||
.mask_texture_slot = 2,
|
||||
.alpha = canvas->m_canvas->m_layers[layer_index]->m_opacity,
|
||||
.mask_enabled = canvas->m_canvas->m_smask_active,
|
||||
});
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
set_active_texture_unit(1);
|
||||
@@ -355,30 +350,29 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
|
||||
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
|
||||
|
||||
ShaderManager::use(kShader::CompDraw);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_int(kShaderUniform::TexStroke, 1);
|
||||
ShaderManager::u_int(kShaderUniform::TexMask, 2);
|
||||
ShaderManager::u_int(kShaderUniform::TexDual, 3);
|
||||
ShaderManager::u_int(kShaderUniform::TexPattern, 4);
|
||||
ShaderManager::u_vec2(kShaderUniform::Resolution, canvas->m_canvas->m_size);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, canvas->m_canvas->m_layers[layer_index]->m_opacity);
|
||||
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
|
||||
ShaderManager::u_int(kShaderUniform::Lock, canvas->m_canvas->m_layers[layer_index]->m_alpha_locked);
|
||||
ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
|
||||
ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled);
|
||||
ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode);
|
||||
ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
|
||||
ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
|
||||
ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert);
|
||||
ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness);
|
||||
ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast);
|
||||
ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth);
|
||||
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
||||
pp::panopainter::setup_legacy_stroke_composite_shader(
|
||||
pp::panopainter::LegacyStrokeCompositeUniforms {
|
||||
.resolution = canvas->m_canvas->m_size,
|
||||
.pattern = {
|
||||
.scale = patt_scale,
|
||||
.invert = static_cast<float>(b->m_pattern_invert),
|
||||
.brightness = b->m_pattern_brightness,
|
||||
.contrast = b->m_pattern_contrast,
|
||||
.depth = b->m_pattern_depth,
|
||||
.blend_mode = b->m_pattern_blend_mode,
|
||||
.offset = Canvas::I->m_pattern_offset,
|
||||
},
|
||||
.mvp = plane_mvp_z,
|
||||
.layer_alpha = canvas->m_canvas->m_layers[layer_index]->m_opacity,
|
||||
.alpha_lock = canvas->m_canvas->m_layers[layer_index]->m_alpha_locked,
|
||||
.mask_enabled = canvas->m_canvas->m_smask_active,
|
||||
.use_fragcoord = false,
|
||||
.blend_mode = b->m_blend_mode,
|
||||
.use_dual = b->m_dual_enabled,
|
||||
.dual_blend_mode = b->m_dual_blend_mode,
|
||||
.dual_alpha = b->m_dual_opacity,
|
||||
.use_pattern = b->m_pattern_enabled && !b->m_pattern_eachsample,
|
||||
});
|
||||
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
@@ -407,11 +401,13 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
else
|
||||
{
|
||||
sampler.bind(0);
|
||||
ShaderManager::use(kShader::TextureAlpha);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, canvas->m_canvas->m_layers[layer_index]->m_opacity);
|
||||
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader(
|
||||
pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
|
||||
.mvp = plane_mvp_z,
|
||||
.texture_slot = 0,
|
||||
.alpha = canvas->m_canvas->m_layers[layer_index]->m_opacity,
|
||||
.highlight = canvas->m_canvas->m_layers[layer_index]->m_hightlight,
|
||||
});
|
||||
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
@@ -434,9 +430,11 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::transpose(canvas->m_canvas->m_cam_rot) *
|
||||
glm::translate(glm::vec3(0, 0, -1)) *
|
||||
glm::scale(aspect * tan_fov);
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 1 });
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
pp::panopainter::setup_legacy_vr_color_shader(
|
||||
pp::panopainter::LegacyVrColorUniforms {
|
||||
.color = { 0, 0, 0, 1 },
|
||||
.mvp = mvp,
|
||||
});
|
||||
m_face_plane.draw_stroke();
|
||||
}
|
||||
|
||||
@@ -484,9 +482,11 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::translate(glm::vec3(0, 0, -1)) *
|
||||
glm::scale(aspect * tan_fov);
|
||||
sampler_linear.bind(0);
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
|
||||
pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
|
||||
.mvp = mvp,
|
||||
.texture_slot = 0,
|
||||
});
|
||||
set_active_texture_unit(0);
|
||||
uirtt.bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
@@ -501,12 +501,17 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::scale(glm::vec3(100)) *
|
||||
glm::transpose(canvas->m_canvas->m_cam_rot) *
|
||||
glm::translate(glm::vec3(cur * glm::vec2(aspect * tan_fov), -1));
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 1 });
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp * glm::scale(glm::vec3(.01)));
|
||||
pp::panopainter::setup_legacy_vr_color_shader(
|
||||
pp::panopainter::LegacyVrColorUniforms {
|
||||
.color = { 0, 0, 0, 1 },
|
||||
.mvp = mvp * glm::scale(glm::vec3(.01f)),
|
||||
});
|
||||
m_face_plane.draw_fill();
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { 1, 1, 1, 1 });
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp * glm::scale(glm::vec3(.005)));
|
||||
pp::panopainter::setup_legacy_vr_color_shader(
|
||||
pp::panopainter::LegacyVrColorUniforms {
|
||||
.color = { 1, 1, 1, 1 },
|
||||
.mvp = mvp * glm::scale(glm::vec3(.005f)),
|
||||
});
|
||||
m_face_plane.draw_fill();
|
||||
}
|
||||
|
||||
@@ -514,10 +519,13 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
// draw the motion controller sphere
|
||||
if (vr_controllers_enabled && ui_visible && ui_inside)
|
||||
{
|
||||
auto mvp = proj * camera * vr_controllers[0].m_mat * glm::eulerAngleX(glm::radians(-30.f));
|
||||
ShaderManager::use(kShader::Color);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, { 1, 0, 1, 1 });
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp * glm::scale(glm::vec3(.0125, .0125, .07)));
|
||||
const auto vr_session = vr_session_snapshot();
|
||||
auto mvp = proj * camera * vr_session.vr_controllers[0].m_mat * glm::eulerAngleX(glm::radians(-30.f));
|
||||
pp::panopainter::setup_legacy_vr_color_shader(
|
||||
pp::panopainter::LegacyVrColorUniforms {
|
||||
.color = { 1, 0, 1, 1 },
|
||||
.mvp = mvp * glm::scale(glm::vec3(.0125f, .0125f, .07f)),
|
||||
});
|
||||
sphere.draw_fill();
|
||||
}
|
||||
|
||||
@@ -525,20 +533,21 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
// draw the motion controller brush
|
||||
if (vr_controllers_enabled && (!ui_visible || !ui_inside))
|
||||
{
|
||||
glm::vec3 cpos = vr_controllers[0].get_pos() - xyz(pose[3]);
|
||||
const auto vr_session = vr_session_snapshot();
|
||||
glm::vec3 cpos = vr_session.vr_controllers[0].get_pos() - xyz(pose[3]);
|
||||
auto pos = glm::translate(glm::normalize(cpos) * 100.f);
|
||||
ShaderManager::use(kShader::StrokePreview);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_float(kShaderUniform::Alpha, canvas->m_canvas->m_current_brush->m_tip_flow);
|
||||
ShaderManager::u_int(kShaderUniform::DrawOutline, false);
|
||||
auto tip_color = glm::vec4(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_color), 1);
|
||||
ShaderManager::u_vec4(kShaderUniform::Col, tip_color);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP,
|
||||
proj * camera * pos *
|
||||
glm::inverse(glm::lookAt({ 0, 0, 0 }, cpos, { 0, 1, 0 })) *
|
||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
|
||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||
);
|
||||
pp::panopainter::setup_legacy_vr_stroke_preview_shader(
|
||||
pp::panopainter::LegacyVrStrokePreviewUniforms {
|
||||
.texture_slot = 0,
|
||||
.alpha = canvas->m_canvas->m_current_brush->m_tip_flow,
|
||||
.draw_outline = false,
|
||||
.color = tip_color,
|
||||
.mvp = proj * camera * pos *
|
||||
glm::inverse(glm::lookAt({ 0, 0, 0 }, cpos, { 0, 1, 0 })) *
|
||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
|
||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0)),
|
||||
});
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||
@@ -567,8 +576,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
mode->on_Draw(ortho_proj, proj, camera);
|
||||
*/
|
||||
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend != 0U);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth != 0U);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth);
|
||||
sampler.unbind();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "asset.h"
|
||||
#include "platform_api/network_tls_policy.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <Foundation/Foundation.h>
|
||||
@@ -8,9 +9,24 @@
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/asset_manager.h>
|
||||
#include <dirent.h>
|
||||
AAssetManager* Asset::m_am;
|
||||
void* Asset::m_android_asset_manager;
|
||||
bool android_create_dir(const std::string& path);
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] AAsset* android_asset_handle(void* asset)
|
||||
{
|
||||
return static_cast<AAsset*>(asset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Asset::set_android_asset_manager(void* asset_manager)
|
||||
{
|
||||
m_android_asset_manager = asset_manager;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Asset::delete_file(const std::string& path)
|
||||
@@ -65,7 +81,7 @@ std::vector<std::string> Asset::list_files(std::string folder, const std::string
|
||||
#elif __ANDROID__
|
||||
if (is_asset)
|
||||
{
|
||||
AAssetDir* dir = AAssetManager_openDir(Asset::m_am, folder.c_str());
|
||||
AAssetDir* dir = AAssetManager_openDir(static_cast<AAssetManager*>(Asset::m_android_asset_manager), folder.c_str());
|
||||
while (const char* name = AAssetDir_getNextFileName(dir))
|
||||
{
|
||||
//LOG("asset: %s", name);
|
||||
@@ -187,9 +203,8 @@ bool Asset::open_url(const std::string& url, std::function<bool(float)> progress
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &tmp_data);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler_asset);
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
|
||||
#ifdef __ANDROID__
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
if (pp::platform::default_disables_network_tls_verification())
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
if (progress)
|
||||
{
|
||||
on_progress = progress;
|
||||
@@ -237,7 +252,10 @@ bool Asset::open(const char* path)
|
||||
#ifdef __ANDROID__
|
||||
if (is_asset(path))
|
||||
{
|
||||
if (!(m_asset = AAssetManager_open(m_am, path, AASSET_MODE_RANDOM)))
|
||||
if (!(m_android_asset = AAssetManager_open(
|
||||
static_cast<AAssetManager*>(m_android_asset_manager),
|
||||
path,
|
||||
AASSET_MODE_RANDOM)))
|
||||
{
|
||||
LOG("AAssetManager_open failed %s", path);
|
||||
return false;
|
||||
@@ -283,8 +301,8 @@ glm::uint8_t* Asset::read_all()
|
||||
{
|
||||
if (is_asset(m_current_path))
|
||||
{
|
||||
m_len = (int)AAsset_getLength(m_asset);
|
||||
m_data = (uint8_t*)AAsset_getBuffer(m_asset);
|
||||
m_len = (int)AAsset_getLength(android_asset_handle(m_android_asset));
|
||||
m_data = (uint8_t*)AAsset_getBuffer(android_asset_handle(m_android_asset));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,9 +334,9 @@ glm::uint8_t* Asset::read_all()
|
||||
void Asset::close()
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
if (m_asset)
|
||||
AAsset_close(m_asset);
|
||||
m_asset = nullptr;
|
||||
if (m_android_asset)
|
||||
AAsset_close(android_asset_handle(m_android_asset));
|
||||
m_android_asset = nullptr;
|
||||
#else
|
||||
if (m_fp)
|
||||
fclose(m_fp);
|
||||
|
||||
@@ -4,8 +4,13 @@ class Asset
|
||||
{
|
||||
public:
|
||||
#ifdef __ANDROID__
|
||||
static AAssetManager* m_am;
|
||||
AAsset* m_asset = nullptr;
|
||||
static void set_android_asset_manager(void* asset_manager);
|
||||
|
||||
private:
|
||||
static void* m_android_asset_manager;
|
||||
void* m_android_asset = nullptr;
|
||||
|
||||
public:
|
||||
#endif
|
||||
static std::vector<std::string> list_files(std::string folder, const std::string& filter_regex);
|
||||
static bool exist(std::string path);
|
||||
|
||||
160
src/assets/brush_package.cpp
Normal file
160
src/assets/brush_package.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "assets/brush_package.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::uint16_t read_u16_le(std::span<const std::byte> bytes, std::size_t offset) noexcept
|
||||
{
|
||||
const auto lo = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset]));
|
||||
const auto hi = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset + 1U]));
|
||||
return static_cast<std::uint16_t>(lo | static_cast<std::uint16_t>(hi << 8U));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_word_extension(std::string_view value) noexcept
|
||||
{
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const char raw : value) {
|
||||
const auto ch = static_cast<unsigned char>(raw);
|
||||
if (std::isalnum(ch) == 0 && ch != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept
|
||||
{
|
||||
if (magic != "PPBR") {
|
||||
return pp::foundation::Status::invalid_argument("PPBR header magic is invalid");
|
||||
}
|
||||
|
||||
// DEBT-0049: preserve legacy version acceptance until PPBR compatibility fixtures exist.
|
||||
if (major != ppbr_legacy_major_version && minor != ppbr_legacy_minor_version) {
|
||||
return pp::foundation::Status::invalid_argument("PPBR version is unsupported");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrHeader> parse_ppbr_header(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < ppbr_header_size) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(
|
||||
pp::foundation::Status::out_of_range("PPBR header is truncated"));
|
||||
}
|
||||
|
||||
const std::string_view magic(reinterpret_cast<const char*>(bytes.data()), 4U);
|
||||
const auto major = read_u16_le(bytes, 4U);
|
||||
const auto minor = read_u16_le(bytes, 6U);
|
||||
const auto status = validate_ppbr_header(magic, major, minor);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpbrHeader>::success(PpbrHeader {
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::string> normalize_ppbr_export_path(std::string_view requested_path)
|
||||
{
|
||||
if (requested_path.empty()) {
|
||||
return pp::foundation::Result<std::string>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must not be empty"));
|
||||
}
|
||||
|
||||
std::string path(requested_path);
|
||||
if (requested_path.find(".ppbr") == std::string_view::npos) {
|
||||
path += ".ppbr";
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::string>::success(std::move(path));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy)
|
||||
{
|
||||
const auto normalized = normalize_ppbr_export_path(requested_path);
|
||||
if (!normalized) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(normalized.status());
|
||||
}
|
||||
|
||||
const auto slash = normalized.value().find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a directory and file name"));
|
||||
}
|
||||
|
||||
const auto dot = normalized.value().find_last_of('.');
|
||||
if (dot == std::string::npos || dot <= slash + 1U || dot + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a file extension"));
|
||||
}
|
||||
|
||||
PpbrExportPaths paths;
|
||||
paths.package_path = normalized.value();
|
||||
paths.directory = normalized.value().substr(0, slash);
|
||||
paths.stem = normalized.value().substr(slash + 1U, dot - slash - 1U);
|
||||
paths.extension = normalized.value().substr(dot + 1U);
|
||||
if (!is_word_extension(paths.extension)) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path extension contains unsupported characters"));
|
||||
}
|
||||
|
||||
if (data_directory_policy == PpbrDataDirectoryPolicy::override_directory) {
|
||||
paths.data_directory = std::string(override_data_directory) + "/" + paths.stem + "_data";
|
||||
} else {
|
||||
paths.data_directory = paths.directory + "/" + paths.stem + "_data";
|
||||
}
|
||||
paths.data_directory_enabled = export_data && !paths.data_directory.empty();
|
||||
|
||||
return pp::foundation::Result<PpbrExportPaths>::success(std::move(paths));
|
||||
}
|
||||
|
||||
pp::foundation::Result<BrushPackageImageTargetPaths> plan_brush_package_image_target_paths(
|
||||
std::string_view data_path,
|
||||
BrushPackageImageKind kind,
|
||||
std::string_view image_name,
|
||||
std::string_view image_extension)
|
||||
{
|
||||
if (data_path.empty()) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package data path must not be empty"));
|
||||
}
|
||||
if (image_name.empty()) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package image name must not be empty"));
|
||||
}
|
||||
if (!is_word_extension(image_extension)) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package image extension contains unsupported characters"));
|
||||
}
|
||||
|
||||
const auto directory = kind == BrushPackageImageKind::brush_tip ? "brushes" : "patterns";
|
||||
const std::string base_path = std::string(data_path) + "/" + directory + "/" + std::string(image_name)
|
||||
+ "." + std::string(image_extension);
|
||||
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::success(BrushPackageImageTargetPaths {
|
||||
.image_path = base_path,
|
||||
.thumbnail_path = std::string(data_path) + "/" + directory + "/thumbs/" + std::string(image_name)
|
||||
+ "." + std::string(image_extension),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::assets
|
||||
69
src/assets/brush_package.h
Normal file
69
src/assets/brush_package.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppbr_header_size = 8;
|
||||
constexpr std::uint16_t ppbr_legacy_major_version = 0;
|
||||
constexpr std::uint16_t ppbr_legacy_minor_version = 1;
|
||||
|
||||
enum class PpbrDataDirectoryPolicy {
|
||||
next_to_package,
|
||||
override_directory,
|
||||
};
|
||||
|
||||
enum class BrushPackageImageKind {
|
||||
brush_tip,
|
||||
pattern,
|
||||
};
|
||||
|
||||
struct PpbrHeader {
|
||||
std::uint16_t major = 0;
|
||||
std::uint16_t minor = 0;
|
||||
};
|
||||
|
||||
struct BrushPackageImageTargetPaths {
|
||||
std::string image_path;
|
||||
std::string thumbnail_path;
|
||||
};
|
||||
|
||||
struct PpbrExportPaths {
|
||||
std::string package_path;
|
||||
std::string directory;
|
||||
std::string stem;
|
||||
std::string extension;
|
||||
std::string data_directory;
|
||||
bool data_directory_enabled = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrHeader> parse_ppbr_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::string> normalize_ppbr_export_path(
|
||||
std::string_view requested_path);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<BrushPackageImageTargetPaths> plan_brush_package_image_target_paths(
|
||||
std::string_view data_path,
|
||||
BrushPackageImageKind kind,
|
||||
std::string_view image_name,
|
||||
std::string_view image_extension);
|
||||
|
||||
} // namespace pp::assets
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user