Compare commits

497 Commits

Author SHA1 Message Date
a44222813f automation: Add missing ui core lifetime tests to platform-build targets 2026-06-07 10:12:18 +02:00
c8b55b36f7 Move project save post-commit planning to app core 2026-06-06 12:16:19 +02:00
f3834827b1 Move project save commit planning to app core 2026-06-06 12:09:36 +02:00
a03db82307 Move project save write planning to app core 2026-06-06 12:00:57 +02:00
ed9709ade8 Move project save target planning to app core 2026-06-06 11:52:49 +02:00
9d9b93abb1 Route live save snapshots through PPI policy 2026-06-06 11:43:50 +02:00
772dc7332b Route equirectangular export writes through app core 2026-06-06 11:31:55 +02:00
9d9c87c0cb Report depth export snapshot routing 2026-06-06 11:24:29 +02:00
09df47879d Move export snapshot platform support to app core 2026-06-06 11:20:25 +02:00
41279c8743 Move export snapshot target support to app core 2026-06-06 11:10:26 +02:00
7575f51c45 Plan document export snapshot routing 2026-06-06 11:03:28 +02:00
6c772a1c84 Share retained GL runtime dispatch adapters 2026-06-06 10:51:46 +02:00
ab36af0a8f Centralize retained panel popup attachment 2026-06-06 10:46:29 +02:00
5ff2992c0e Centralize retained menu popup attachment 2026-06-06 10:37:14 +02:00
65c7716d62 Add quiet validation wrapper 2026-06-06 10:03:01 +02:00
59c9b05d6c Centralize retained UI overlay insertion 2026-06-06 09:55:02 +02:00
3101e65dd3 Model UI overlay lifetime in ui core 2026-06-06 09:48:00 +02:00
4071919124 Model UI capture lifetime in ui core 2026-06-06 09:40:21 +02:00
d963daae70 Add UI core node lifetime handles 2026-06-06 09:00:24 +02:00
7a9dd150e3 Track UI lifetime safety in roadmap 2026-06-06 07:58:58 +02:00
bd416f8473 Export equirectangular JPEGs through paint renderer 2026-06-05 21:22:06 +02:00
875a0127d9 Report depth export readiness in CLI snapshots 2026-06-05 21:09:03 +02:00
3be7171010 Plan depth export through document renderer 2026-06-05 21:03:27 +02:00
3c36be4b43 Export layer collections through paint renderer 2026-06-05 20:48:16 +02:00
77268a28fb Export equirectangular PNGs through paint renderer 2026-06-05 20:31:35 +02:00
ebc84373e6 Share document export readiness reporting 2026-06-05 20:19:46 +02:00
2d33f9d928 Dispatch cube export writes through app core 2026-06-05 20:09:46 +02:00
af28da4e83 Plan cube export face targets in app core 2026-06-05 20:00:25 +02:00
27e7c60413 Write cube exports from document snapshots 2026-06-05 19:48:56 +02:00
6151fb7a3d Export document frame faces as PNGs 2026-06-05 18:54:27 +02:00
693923b7bd Share recorded document upload reporting 2026-06-05 18:46:15 +02:00
81898a5dcc Prepare renderer snapshots before legacy exports 2026-06-05 18:38:45 +02:00
9cafc39788 Render captured canvas snapshots through renderer boundary 2026-06-05 18:31:39 +02:00
ba5c3069e1 Export captured canvas snapshots through document writer 2026-06-05 18:24:58 +02:00
9a75782891 Prepare document snapshots before legacy saves 2026-06-05 18:14:50 +02:00
f4f6eb903e Attach captured canvas payloads to document snapshots 2026-06-05 18:03:33 +02:00
d0412e3bf9 Project legacy canvas metadata into documents 2026-06-05 17:54:45 +02:00
a9ef2c598c Plan document frame uploads for OpenGL 2026-06-05 17:42:31 +02:00
d0e023556b Upload document frame faces through renderer API 2026-06-05 17:37:21 +02:00
7c6c5f3e36 Add document frame render automation 2026-06-05 17:30:44 +02:00
d4dad133ea Add document face compositor bridge 2026-06-05 17:09:17 +02:00
ee46a6497f Deploy OpenVR runtime for Windows app 2026-06-05 16:53:27 +02:00
26a2349c5f Fix VS 2026 Windows build 2026-06-05 16:38:06 +02:00
0e6c61e8a9 Fix Windows MSVC preset generator 2026-06-05 16:21:34 +02:00
bdd7a32ff5 Prefer OpenXR for desktop XR policy 2026-06-05 16:06:52 +02:00
308fb13075 Share retained runtime GL dispatch 2026-06-05 15:30:33 +02:00
0fb3bd09ac Share retained RTT clear dispatch 2026-06-05 15:27:00 +02:00
26470e0fe8 Share retained app UI GL dispatch 2026-06-05 15:19:10 +02:00
96d1903cf2 Share retained utility GL dispatch bridges 2026-06-05 15:05:07 +02:00
ad9b91eeda Share retained shader dispatch bridge 2026-06-05 14:58:20 +02:00
96ff1c41e2 Share retained mesh dispatch bridge 2026-06-05 14:51:40 +02:00
d719a5a5e5 Share retained sampler and pixel-buffer dispatch bridges 2026-06-05 14:42:40 +02:00
84e63c0d34 Share retained framebuffer dispatch bridge 2026-06-05 14:35:45 +02:00
421f2713db Share retained texture dispatch bridge 2026-06-05 14:29:59 +02:00
df21d673dd Share retained renderbuffer dispatch bridge 2026-06-05 14:22:21 +02:00
76a8db1ef8 Share Canvas state GL dispatch adapters 2026-06-05 14:13:04 +02:00
65bf047d77 Share target-aware framebuffer copy bridge 2026-06-05 14:07:46 +02:00
2641db35ac Share CanvasLayer GL dispatch adapters 2026-06-05 14:00:33 +02:00
745a5898da Share grid GL dispatch adapters 2026-06-05 13:53:51 +02:00
03b999e60f Share CanvasMode GL dispatch adapters 2026-06-05 13:44:57 +02:00
92fa5b224a Centralize retained UI GL dispatch 2026-06-05 13:34:59 +02:00
8f062fb0c4 Expose Linux WebGL package readiness target 2026-06-05 13:25:52 +02:00
321e5d6287 Add Linux app package readiness 2026-06-05 13:22:57 +02:00
35477978e5 Modernize retained Linux WebGL CMake baselines 2026-06-05 13:16:54 +02:00
8a4ca331cb Install latest Android SDK toolchain packages 2026-06-05 13:12:30 +02:00
e731c06330 Add CMake vcpkg UI validation target 2026-06-05 13:04:34 +02:00
3e5340b696 Expose Windows package smoke as CMake target 2026-06-05 12:51:25 +02:00
97fd7de955 Expose platform checks as CMake targets 2026-06-05 12:47:32 +02:00
1dc2ae4f21 Expose package checks as CMake targets 2026-06-05 12:40:22 +02:00
711a9b5037 Add Android native package smoke checks 2026-06-05 12:35:47 +02:00
c761cd39fd Use latest Android SDK toolchain 2026-06-05 12:28:47 +02:00
ac4fef8346 Refresh retained Android package CMake 2026-06-05 12:17:04 +02:00
e17463bf5a Hide Android asset SDK handles 2026-06-05 11:54:02 +02:00
0236fc6620 Split asset reload platform policy 2026-06-05 11:44:28 +02:00
c4d00258ff Catalog native UI state platform policy 2026-06-05 11:41:05 +02:00
b1d71f2621 Extract platform policy catalog 2026-06-05 11:39:21 +02:00
ab3637af9c Fix Apple remote wrapper shell transport 2026-06-05 11:19:07 +02:00
48f98d337b Add Apple remote compile gate 2026-06-05 11:13:28 +02:00
b534c4a4da Plan recording export progress dialog 2026-06-05 10:15:33 +02:00
407297dc2e Plan main toolbar message dialog 2026-06-05 10:10:31 +02:00
903fe2d5a1 Route cloud prompts through app dialog bridge 2026-06-05 10:03:34 +02:00
73564342fc Route document session prompts through app dialog bridge 2026-06-05 09:59:12 +02:00
d9f294e8e6 Bridge app dialog creation 2026-06-05 09:53:53 +02:00
f225a81ec4 Plan PPBR export reporting 2026-06-05 09:49:32 +02:00
fcc0e577b8 Plan document export reporting 2026-06-05 09:43:16 +02:00
808a084ee3 Plan document export success messages 2026-06-05 09:31:11 +02:00
f46839bf5c Plan cloud dialog metadata 2026-06-05 08:14:11 +02:00
e5526c6d0a Plan document session prompts 2026-06-05 08:07:54 +02:00
5def47cdcc Plan canvas toolbar bindings 2026-06-05 07:52:58 +02:00
062fdaa982 Plan app dialog factories 2026-06-05 07:36:56 +02:00
a79ef4cda8 Plan cloud transfer requests 2026-06-05 07:27:51 +02:00
a104f88360 Plan UI observer frame clipping 2026-06-05 07:18:48 +02:00
942c053c19 Plan recording worker wake decisions 2026-06-05 07:07:28 +02:00
c50ea14a2a Route app thread orchestration through app core 2026-06-05 07:01:51 +02:00
32c95b224f Extend app input planning to UI state 2026-06-05 06:44:57 +02:00
b825d920d2 Route app input dispatch through app core 2026-06-05 06:38:48 +02:00
1df93c23f7 Route command conversion through app core 2026-06-05 06:31:38 +02:00
548b6d3ae5 Extend app frame planning to tick and resize 2026-06-05 06:23:00 +02:00
48a4547f51 Route app shutdown staging through app core 2026-06-05 06:13:52 +02:00
f7979be80f Route app frame decisions through app core 2026-06-05 06:08:39 +02:00
678bf2dcd6 Route startup resources through app core 2026-06-05 05:55:23 +02:00
e42afcc83f Route canvas view execution through app core 2026-06-05 05:47:42 +02:00
9373e07d3e Route canvas camera reset through app core 2026-06-05 01:46:41 +02:00
f42a6540be Route canvas cursor visibility through app core 2026-06-05 01:39:36 +02:00
e95861e9b7 Route quick slider preview through app core 2026-06-05 01:26:02 +02:00
31c26c3127 Route brush refresh view through app core 2026-06-05 01:13:34 +02:00
d5403f082c Route stroke panel view through app core 2026-06-05 01:01:56 +02:00
75fd7faeb0 Route layer panel view through app core 2026-06-05 00:50:37 +02:00
bd6cdc20c5 Route animation panel view through app core 2026-06-05 00:37:11 +02:00
a9e12f2219 Route animation timeline scrubbing through app core 2026-06-05 00:28:06 +02:00
59210c28ea Route onion frame planning through app core 2026-06-05 00:19:12 +02:00
2feeffd6c8 Expand Android platform build coverage 2026-06-05 00:14:04 +02:00
841fbac8eb Add package readiness automation guard 2026-06-05 00:07:36 +02:00
db0ecb590c Guard platform build target matrix 2026-06-05 00:03:17 +02:00
4ccedaae4c Align platform build target matrix 2026-06-05 00:01:17 +02:00
c514ac99aa Route VR texture state through GL dispatch 2026-06-04 23:54:15 +02:00
3cd1d46025 Route retained draw state through GL dispatch 2026-06-04 23:50:50 +02:00
111cc8c892 Route RTT texture updates through GL backend 2026-06-04 23:44:25 +02:00
c9fb91ab48 Route UI capability queries through GL backend 2026-06-04 23:29:30 +02:00
2a4698e9f6 Route retained readbacks through GL backend 2026-06-04 23:20:06 +02:00
9190e9053a Route canvas clear state through GL backend 2026-06-04 23:13:21 +02:00
b8c7cd6e99 Route paint UI clear state through GL backend 2026-06-04 23:05:19 +02:00
b65db6f617 Route UI leaf render state through GL dispatch 2026-06-04 22:53:07 +02:00
7ade927beb Route canvas mode render state through GL backend 2026-06-04 22:44:47 +02:00
d0510e9fd2 Route grid render state through GL backend 2026-06-04 22:38:06 +02:00
5aa07b2953 Route canvas layer GL state through backend 2026-06-04 22:28:49 +02:00
d55f26d637 Route paint render state through GL backend 2026-06-04 22:19:54 +02:00
24197c5f7e Document regex code navigation rule 2026-06-04 22:14:37 +02:00
abe3a86cc5 Route paint texture unit binding through GL backend 2026-06-04 22:08:46 +02:00
4c61a490ce Route RTT utility clears through GL backend 2026-06-04 21:58:27 +02:00
ce787ce186 Route RTT lifecycle through GL backend 2026-06-04 21:47:19 +02:00
fc20851462 Route PBO readbacks through GL backend 2026-06-04 21:29:49 +02:00
45802dfc7c Document regex code navigation workflow 2026-06-04 21:16:36 +02:00
6440bde002 Route framebuffer texture copies through GL backend 2026-06-04 21:12:46 +02:00
15c58bfb21 Route RTT region readbacks through backend 2026-06-04 21:01:13 +02:00
b9dbcd10d7 Move canvas depth renderbuffers into GL backend 2026-06-04 20:50:44 +02:00
f55b1882c0 Route VR and startup GL state through backend 2026-06-04 20:37:38 +02:00
967a15f15f Move convert GL state into renderer backend 2026-06-04 20:27:43 +02:00
51601adf6d Move render debug state setup into GL backend 2026-06-04 20:12:54 +02:00
1b771287f9 Document regex code navigation workflow 2026-06-04 20:03:15 +02:00
0bd1e92ee1 Route renderer feature gates through device snapshot 2026-06-04 19:58:38 +02:00
f2cb0f2276 Move shader feature negotiation into renderer backend 2026-06-04 19:49:06 +02:00
1057dd488a Move OpenGL extension query into renderer backend 2026-06-04 19:39:06 +02:00
11c7d87330 Move OpenGL runtime classification into renderer backend 2026-06-04 19:28:34 +02:00
0489c4229e Route default canvas resolution through platform services 2026-06-04 19:20:06 +02:00
b2334e65c9 Hide Android asset manager state from asset header 2026-06-04 19:09:39 +02:00
e6831fcb28 Document regex-aware code navigation skill 2026-06-04 19:04:52 +02:00
63ea626cef Isolate platform SDK includes from app header 2026-06-04 18:58:02 +02:00
08d8c1e82c Move layout reload policy into platform api 2026-06-04 18:49:48 +02:00
7aadd1041a Route VR lifecycle through platform services 2026-06-04 18:39:22 +02:00
4bd29bee9f Route canvas input policy through platform services 2026-06-04 18:26:21 +02:00
e52fd3cbb5 Use shared parallel loop for grid lightmap bake 2026-06-04 18:17:40 +02:00
b1acd5118b Route dialog work directory picker through platform services 2026-06-04 18:14:02 +02:00
52cf7628da Route PPBR export directory policy through platform services 2026-06-04 18:01:50 +02:00
148aceb705 Centralize legacy network TLS policy 2026-06-04 17:53:49 +02:00
c698de1482 Route SonarPen tools action through platform services 2026-06-04 17:47:08 +02:00
883be98557 Route app network TLS policy through platform services 2026-06-04 17:35:24 +02:00
401ce33498 Route collection export targets through platform policy 2026-06-04 17:24:36 +02:00
be4b88dec8 Add regex filters to clangd navigation 2026-06-04 17:12:14 +02:00
104358bc62 Route prepared export policy through platform services 2026-06-04 17:05:49 +02:00
cabfa44729 Route prepared file targets through platform services 2026-06-04 16:58:05 +02:00
dc369c89b0 Route UI state save through platform services 2026-06-04 16:48:57 +02:00
7d992931d9 Route document browse roots through platform services 2026-06-04 16:41:56 +02:00
6419645e03 Route export storage hooks through platform services 2026-06-04 16:34:19 +02:00
3c709f07e6 Add regex filtering to clangd navigation 2026-06-04 16:15:54 +02:00
ca452f46e1 Guard incomplete clangd references 2026-06-04 16:10:42 +02:00
576b58b061 Add clangd navigation helper 2026-06-04 16:03:03 +02:00
bc3973ef15 Document agent modernization handoff 2026-06-04 15:31:53 +02:00
0c7bc98d5b Add brush preset list executor bridge 2026-06-04 15:25:54 +02:00
47c35fb859 Route brush preset list planning 2026-06-04 15:15:01 +02:00
79942113ef Extract brush package import targets 2026-06-04 14:59:38 +02:00
394979e4fc Extract PPBR package path validation 2026-06-04 14:56:29 +02:00
6ab64ccc82 Centralize legacy brush package import 2026-06-04 14:49:22 +02:00
78185b8fd5 Centralize legacy brush package export 2026-06-04 14:44:37 +02:00
2bd1b12ade Centralize legacy app startup 2026-06-04 14:32:39 +02:00
884a6d4940 Route VR mode through app preferences 2026-06-04 14:22:39 +02:00
f8243566c4 Centralize legacy app preferences 2026-06-04 14:18:18 +02:00
ca5b94b044 Centralize legacy video export bridge 2026-06-04 14:11:24 +02:00
78003923ca Centralize legacy document image exports 2026-06-04 13:57:32 +02:00
ab6223c256 Centralize legacy document file saves 2026-06-04 13:47:43 +02:00
8a0810acb3 Centralize legacy new document bridge 2026-06-04 13:38:52 +02:00
4528edfb2c Centralize legacy document session bridge 2026-06-04 13:30:22 +02:00
d980b81bd7 Centralize legacy document open bridge 2026-06-04 13:20:14 +02:00
1984b71a0a Centralize legacy cloud bridge 2026-06-04 13:09:45 +02:00
a9ed201adf Centralize legacy recording bridge 2026-06-04 12:58:27 +02:00
65e9fdf1b9 Centralize quick and grid UI bridges 2026-06-04 12:43:00 +02:00
bd2ee54617 Centralize legacy animation bridge 2026-06-04 12:35:20 +02:00
a2e795a356 Centralize legacy brush UI bridge 2026-06-04 12:18:26 +02:00
c3d85074ac Centralize legacy canvas tool bridge 2026-06-04 11:59:20 +02:00
22bbc93b43 Centralize legacy app shell services 2026-06-03 21:32:15 +02:00
7460453b80 Centralize legacy document layer bridge 2026-06-03 21:16:07 +02:00
855c388027 Centralize legacy document canvas bridge 2026-06-03 20:58:52 +02:00
d1bd4e9b46 Centralize legacy history bridge 2026-06-03 20:45:33 +02:00
6945ce7e23 Route canvas hotkeys through app core 2026-06-03 20:30:07 +02:00
16a1d1e15b Route layer merge through app core 2026-06-03 20:20:07 +02:00
b184b3e075 Route layer menu clear through app core 2026-06-03 20:13:07 +02:00
10c995f1da Dispatch layer rename through app core 2026-06-03 20:06:11 +02:00
921fc8f00b Dispatch layer operations through app core 2026-06-03 20:00:15 +02:00
164f99fe48 Plan renderer diagnostics in app status 2026-06-03 19:49:54 +02:00
fa1493b843 Plan stroke preview feedback copies 2026-06-03 19:42:15 +02:00
6b92d0bfea Plan thumbnail blend feedback copies 2026-06-03 19:27:57 +02:00
2ac2c45b11 Plan canvas stroke feedback copies 2026-06-03 18:52:37 +02:00
b576143afb Use blend gate plan for canvas copy decisions 2026-06-03 18:37:58 +02:00
bc5b39057d Publish renderer feature snapshot for canvas gates 2026-06-03 18:32:17 +02:00
1369a9048e Centralize canvas blend gate planning 2026-06-03 18:20:01 +02:00
a89f5e6cf2 Route canvas blend gate through paint renderer 2026-06-03 18:14:37 +02:00
2ec11e5099 Add stroke composite feedback planner 2026-06-03 18:07:08 +02:00
94a6877e7c Add paint feedback strategy planner 2026-06-03 17:58:24 +02:00
dc23a5648d Add brush stroke control boundary 2026-06-03 17:42:09 +02:00
9adfad9609 Add brush texture list boundary 2026-06-03 17:21:49 +02:00
cee5f141a3 Add animation panel action planner 2026-06-03 16:57:56 +02:00
603bb0c4e7 Add animation playback toggle boundary 2026-06-03 16:47:02 +02:00
5752bc6ae9 Extend animation panel frame dispatch 2026-06-03 16:39:14 +02:00
93f3037410 Add animation panel service boundary 2026-06-03 14:04:36 +02:00
9c7c89fed4 Add canvas tool service boundary 2026-06-03 13:42:34 +02:00
45a7d49d40 Add quick panel service boundary 2026-06-03 13:36:48 +02:00
de9bca8bb5 Add brush UI service boundary 2026-06-03 13:28:50 +02:00
6427f218e7 Add history command service boundary 2026-06-03 13:22:16 +02:00
6d0cc4eb15 Add image import service boundary 2026-06-03 13:17:31 +02:00
a6306c2759 Add canvas clear service boundary 2026-06-03 13:13:43 +02:00
7c76703355 Add document resize service boundary 2026-06-03 13:09:12 +02:00
9c3f56954e Add export menu service boundary 2026-06-03 13:04:00 +02:00
e880f23040 Add file menu service boundary 2026-06-03 13:00:22 +02:00
defa9fc212 Add layer menu service boundary 2026-06-03 12:56:25 +02:00
ea96f38875 Add tools menu service boundary 2026-06-03 12:52:46 +02:00
b67f3d63cf Add about menu service boundary 2026-06-03 12:47:15 +02:00
fb111dcdc9 Add main toolbar service boundary 2026-06-03 12:42:23 +02:00
62561624ed Extract main toolbar action planning 2026-06-03 12:37:32 +02:00
b5bd6d42f7 Extract about menu action planning 2026-06-03 12:27:47 +02:00
c640519772 Extract tools menu planning 2026-06-03 12:15:57 +02:00
fb844f79fd Extract layer menu action planning 2026-06-03 12:05:13 +02:00
6dac909869 Extract file menu action planning 2026-06-03 11:56:14 +02:00
65b262207c Extract export menu action planning 2026-06-03 11:48:34 +02:00
ef50f4a361 Extract image import route planning 2026-06-03 11:41:28 +02:00
888e94a77c Extract canvas clear command planning 2026-06-03 11:35:20 +02:00
c56d301b29 Route canvas input mode switching through planner 2026-06-03 11:28:41 +02:00
91e1c2c9a3 Extract canvas toolbar state planning 2026-06-03 11:26:58 +02:00
2087505921 Extract canvas tool UI planning 2026-06-03 11:20:56 +02:00
58afa672c7 Extract history UI operation planning 2026-06-03 11:13:57 +02:00
8dc476d205 Extract quick UI operation planning 2026-06-03 11:01:01 +02:00
73fac0f8e4 Extract grid UI operation planning 2026-06-03 10:52:51 +02:00
a487b0ba48 Mute unused parameter warnings 2026-06-03 10:46:25 +02:00
efd568a416 Extract brush UI operation planning 2026-06-03 10:40:12 +02:00
4f0909f30c Extract animation operation planning 2026-06-03 10:32:06 +02:00
fdc1defaba Extract layer operation planning 2026-06-03 10:20:37 +02:00
07ed23c2d1 Extract layer rename planning 2026-06-03 10:10:08 +02:00
5d5bb24711 Extract document resize planning 2026-06-03 10:03:34 +02:00
21c448d6f1 Report package smoke readiness matrix 2026-06-03 09:57:12 +02:00
cd9206344d Align platform build presets and component matrix 2026-06-03 09:51:14 +02:00
4d06608cc9 Extract app status planning into app core 2026-06-03 09:45:12 +02:00
a64a63def7 Extract app preference planning into app core 2026-06-03 09:36:38 +02:00
19cb14b5dc Debt-track MSVC unreferenced parameter warning mute 2026-06-03 08:45:29 +02:00
4de6f496ad Route font mesh operations through renderer GL 2026-06-03 07:29:09 +02:00
e1cce05bd6 Route Shape mesh operations through renderer GL 2026-06-03 07:19:14 +02:00
1ae79ab3c1 Route shader creation through renderer GL 2026-06-03 07:03:50 +02:00
acdaf3bb8e Route shader runtime calls through renderer GL 2026-06-03 06:53:51 +02:00
f20595aff6 Route cube textures and samplers through renderer GL 2026-06-03 06:46:06 +02:00
779d6b0387 Route RTT framebuffer binding through renderer GL 2026-06-03 06:35:47 +02:00
3128a0d309 Route RTT blit readback through renderer GL 2026-06-03 06:31:41 +02:00
ae69f7437f Route Texture2D through renderer GL 2026-06-03 06:24:56 +02:00
9971b2b7f2 Route GL state snapshot through renderer GL 2026-06-03 06:15:51 +02:00
3e15b2f46c Route VR render state through renderer GL 2026-06-03 06:03:28 +02:00
7dcf76c3aa Route VR UI viewport scissor through renderer GL 2026-06-03 05:59:36 +02:00
155e67fcec Route app viewport scissor through renderer GL 2026-06-03 05:56:57 +02:00
2a030318b1 Route default clear through renderer GL 2026-06-03 05:50:36 +02:00
103fe4fb12 Route GL runtime info through renderer GL 2026-06-03 05:45:08 +02:00
b2335b1656 Apply startup GL state through renderer GL 2026-06-03 05:41:27 +02:00
692fe08d9f Move initial GL state policy into renderer GL 2026-06-03 05:35:39 +02:00
8b12ae35d4 Route render debug callback through platform services 2026-06-03 05:30:54 +02:00
87b1851d59 Route render platform hints through platform services 2026-06-03 05:20:24 +02:00
389cd93e68 Route render target binding through platform services 2026-06-03 05:17:25 +02:00
6652127545 Route diagnostic hooks through platform services 2026-06-03 05:12:00 +02:00
e152616d7f Route live reload policy through platform services 2026-06-03 05:08:00 +02:00
ac4d065c78 Route recording cleanup through platform services 2026-06-03 05:04:14 +02:00
578b1f6082 Route startup storage paths through platform services 2026-06-03 04:59:23 +02:00
beb7f717f1 Route render capture hooks through platform services 2026-06-03 04:54:29 +02:00
7a9b14a86f Route render context lifecycle through platform services 2026-06-03 04:50:42 +02:00
f3925f8423 Route UI thread lifecycle through platform services 2026-06-03 04:45:02 +02:00
dd641c047b Route platform frame hooks through services 2026-06-03 04:41:01 +02:00
22006eaf47 Route native close through platform services 2026-06-03 04:36:10 +02:00
537f0dcb2f Dispatch visibility through platform services 2026-06-03 04:31:30 +02:00
2ea850cbcc Route prepared file saves through platform services 2026-06-03 04:29:58 +02:00
e10e16f491 Extract legacy platform services adapter 2026-06-03 04:25:01 +02:00
6369c3c969 Extract Windows platform services 2026-06-03 04:20:14 +02:00
ead7f58285 Inject Windows platform services 2026-06-03 04:14:47 +02:00
0e77ca6ba8 Route picker callbacks through platform services 2026-06-03 04:07:09 +02:00
1e0500a3f7 Route file actions through platform services 2026-06-03 04:03:25 +02:00
4ed72ebc80 Introduce platform services interface 2026-06-03 03:59:59 +02:00
6960bd3410 Plan clipboard text actions in app core 2026-06-03 03:52:00 +02:00
5ee2dd271c Plan cursor visibility in app core 2026-06-03 03:47:28 +02:00
5ac807c6bd Plan keyboard visibility in app core 2026-06-03 03:42:47 +02:00
4af55a7d3f Plan display file actions in app core 2026-06-03 03:38:14 +02:00
712c28068d Plan picked path callbacks in app core 2026-06-03 03:33:33 +02:00
777723b68c Plan document share decisions in app core 2026-06-02 23:53:09 +02:00
cc3490d9d8 Plan recording session decisions in app core 2026-06-02 23:49:13 +02:00
d9be3f910a Plan cloud bulk upload progress in app core 2026-06-02 23:42:27 +02:00
8a7db3bca8 Plan cloud browse decisions in app core 2026-06-02 23:39:03 +02:00
3a78361aea Plan cloud upload decisions in app core 2026-06-02 23:34:58 +02:00
6e3296469a Route video export start through app core 2026-06-02 23:27:41 +02:00
561193b2ab Plan export start decisions in app core 2026-06-02 23:24:44 +02:00
8de9dadf1d Plan save-as file writes in app core 2026-06-02 23:18:19 +02:00
853307697a Plan new document creation in app core 2026-06-02 23:14:35 +02:00
fd1772a417 Plan document open actions in app core 2026-06-02 23:06:36 +02:00
1df506a176 Plan save-version targets in app core 2026-06-02 22:58:28 +02:00
b349f24931 Plan app export targets in app core 2026-06-02 22:50:42 +02:00
5841878df9 Plan document file saves in app core 2026-06-02 22:42:51 +02:00
c8d769c02c Route document workflow prompts through app core 2026-06-02 22:36:05 +02:00
d28aa25358 Route app save decisions through app core 2026-06-02 22:26:58 +02:00
76808d60e3 Extract app document session decisions 2026-06-02 22:21:08 +02:00
9dd53f9212 Expose app document routing in pano cli 2026-06-02 22:16:08 +02:00
0e03e5940a Add app document route core 2026-06-02 22:10:50 +02:00
e15894e4ea Contain retained assets and paint document paths 2026-06-02 22:05:19 +02:00
37b1cf82f3 Contain retained base UI controls 2026-06-02 22:00:56 +02:00
39444af84e Contain retained vendor source bundle 2026-06-02 21:58:27 +02:00
da584ce0f0 Contain retained OpenGL runtime sources 2026-06-02 21:56:22 +02:00
455c91bf29 Split retained legacy engine target 2026-06-02 21:53:17 +02:00
6fda4d4a90 Move app orchestration into app target 2026-06-02 21:50:07 +02:00
b84dfc049d Move app UI workflows into UI target 2026-06-02 21:48:17 +02:00
3a1ca7a8e6 Extract first PanoPainter UI target 2026-06-02 21:45:49 +02:00
b80bd759aa Move version metadata into app target 2026-06-02 21:40:30 +02:00
a2e47c862e Extract Windows runtime payload deployment 2026-06-02 21:36:31 +02:00
7b882896f1 Move Windows link ownership to platform shell 2026-06-02 21:29:29 +02:00
def1a170dc Split Windows app shell target 2026-06-02 21:27:46 +02:00
6a3cd867f0 Validate OpenGL command pass barriers 2026-06-02 21:16:58 +02:00
55b725e876 Validate OpenGL texture binding slots 2026-06-02 21:14:07 +02:00
d664e9fc39 Validate OpenGL command plan dependencies 2026-06-02 21:06:44 +02:00
1dcd96ab36 Plan OpenGL shader command metadata 2026-06-02 21:02:24 +02:00
b6a25474ff Plan OpenGL texture command metadata 2026-06-02 20:59:05 +02:00
b4c2117992 Expose OpenGL command plans in pano_cli 2026-06-02 20:52:53 +02:00
9a4c595f64 Plan recorded OpenGL command streams 2026-06-02 20:48:49 +02:00
ce33eaaef2 Plan recorded renderer commands for OpenGL 2026-06-02 20:45:03 +02:00
cc33fbdde2 Map render pass clear values to OpenGL 2026-06-02 20:35:28 +02:00
c18297f221 Map renderer sampler state to OpenGL 2026-06-02 20:33:20 +02:00
2f8f12a8fd Map renderer viewport and scissor to OpenGL 2026-06-02 20:29:30 +02:00
728116da8f Map renderer blend state to OpenGL 2026-06-02 20:26:55 +02:00
36f9e73dd4 Map renderer depth state to OpenGL 2026-06-02 20:23:49 +02:00
9b6c5b0849 Map render pass clear masks to OpenGL 2026-06-02 20:19:54 +02:00
cc4eaef3e6 Map renderer color write masks to OpenGL 2026-06-02 20:15:56 +02:00
77c2a68cc5 Map renderer blit filters to OpenGL 2026-06-02 18:30:46 +02:00
647dd81992 Map renderer sampler tokens to OpenGL 2026-06-02 18:28:25 +02:00
c5c31f0a56 Map renderer depth compares to OpenGL 2026-06-02 18:25:25 +02:00
b6c66f3e41 Map renderer blend tokens to OpenGL 2026-06-02 18:21:52 +02:00
1065183e75 Map renderer primitive topologies to OpenGL 2026-06-02 18:18:34 +02:00
dc03491b0d Map renderer texture formats to OpenGL 2026-06-02 18:14:40 +02:00
8c99454bf5 Check OpenGL readback byte counts 2026-06-02 18:09:45 +02:00
0fc73d51d2 Validate renderer blit descriptors first 2026-06-02 18:06:23 +02:00
831e5deeae Validate renderer readback descriptors 2026-06-02 18:03:42 +02:00
22dfde8e7c Validate shader resource label bounds 2026-06-02 18:00:30 +02:00
9a7f4bc0d2 Expose recording renderer clear reset automation 2026-06-02 17:57:51 +02:00
860e5ad31e Reset recording renderer state on clear 2026-06-02 17:53:17 +02:00
9b00acec6f Reject non-finite CLI float inputs 2026-06-02 17:48:13 +02:00
53fc5f9a57 Reject duplicate document snapshot payloads 2026-06-02 17:45:29 +02:00
f6780d183c Expose fuzz and stress test presets 2026-06-02 17:42:23 +02:00
48fdfd849d Reject unsupported parsed PPI blend modes 2026-06-02 17:39:45 +02:00
52da64fc96 Reject non-finite PPI layer opacity 2026-06-02 17:37:32 +02:00
9759abde44 Harden binary stream overlapping writes 2026-06-02 17:35:00 +02:00
06a44705d0 Harden stroke sampler edge tests 2026-06-02 17:32:12 +02:00
3ae84de123 Expose paint blend references in CLI 2026-06-02 17:27:41 +02:00
8c0784f9c3 Add stroke alpha blend reference tests 2026-06-02 17:23:44 +02:00
995752da75 Add renderer backend feature reporting 2026-06-02 17:18:48 +02:00
18617cdbd2 Add renderer texture transition contract 2026-06-02 17:13:44 +02:00
56cb9eaacb Expose renderer mipmap command in CLI 2026-06-02 17:06:31 +02:00
a5dbf05ab5 Add renderer resource label contract 2026-06-02 17:01:10 +02:00
bbe3db1747 Add renderer trace scope contract 2026-06-02 16:55:23 +02:00
07293c0590 Add renderer mipmap command contract 2026-06-02 16:47:44 +02:00
901aff1051 Add renderer texture usage contract 2026-06-02 16:42:53 +02:00
75dd5cfdc9 Add renderer texture copy command 2026-06-02 16:35:38 +02:00
483bbb4a9c Add renderer draw descriptor contract 2026-06-02 16:27:28 +02:00
58f163788b Add renderer render pass clear contract 2026-06-02 16:23:02 +02:00
8232b0efc8 Add renderer shader uniform command 2026-06-02 16:15:23 +02:00
23c308db1b Add renderer resource factory contract 2026-06-02 16:09:52 +02:00
881b5271a2 Capture renderer draw mesh inputs 2026-06-02 16:01:29 +02:00
952a00e7d3 Add renderer sampler state contract 2026-06-02 15:56:26 +02:00
b68ddc42c6 Add renderer depth state contract 2026-06-02 15:50:59 +02:00
9a7e1c4def Add renderer scissor state contract 2026-06-02 15:46:03 +02:00
5226746c1a Add renderer blend state contract 2026-06-02 15:40:43 +02:00
5dbeb0504d Add renderer texture binding contract 2026-06-02 15:34:57 +02:00
ee3fb36047 Add renderer blit command contract 2026-06-02 15:31:01 +02:00
1c40602744 Add renderer texture upload contract 2026-06-02 15:25:31 +02:00
818014127a Add renderer frame capture contract 2026-06-02 15:18:04 +02:00
d37145660a Harden recording renderer CLI bounds 2026-06-02 15:12:59 +02:00
c58b9a3718 Add renderer readback command contract 2026-06-02 15:10:44 +02:00
a6a4e7b249 Add stroke script CLI edge coverage 2026-06-02 11:34:17 +02:00
2b50c2157f Add stroke script document automation 2026-06-02 11:31:08 +02:00
0eded78c4c Add document export file automation 2026-06-02 11:25:07 +02:00
99b2eeb99d Add document export CLI automation 2026-06-02 11:18:19 +02:00
7b14c356db Validate snapshot face payloads 2026-06-02 11:14:41 +02:00
bad2670f87 Add document PPI export boundary 2026-06-02 11:11:01 +02:00
b3710498f3 Add explicit PPI project writer 2026-06-02 11:05:08 +02:00
1bc90d88b4 Add targeted PPI payload automation 2026-06-02 11:00:29 +02:00
ddca24779e Add document selection mask automation 2026-06-02 10:55:12 +02:00
1ab2a9b846 Add PPI layer metadata save automation 2026-06-02 10:48:03 +02:00
9c6b52eb8e Add image export roundtrip automation 2026-06-02 10:41:34 +02:00
9d05d193a7 Add positive import image automation 2026-06-02 10:35:40 +02:00
e6e80b94ba Add multi-layer PPI save automation 2026-06-02 10:32:10 +02:00
4e70c90ca8 Add multi-frame PPI save automation 2026-06-02 10:25:30 +02:00
a8faa82b70 Add PPI dirty-face payload save automation 2026-06-02 10:18:35 +02:00
4f4ac380ac Update PPI save modernization debt 2026-06-02 10:11:36 +02:00
374cb5b075 Add metadata-only PPI save automation 2026-06-02 10:10:30 +02:00
b0445382dd Add file-driven image import automation 2026-06-02 10:04:48 +02:00
3701fd2a71 Expose image import simulation through pano cli 2026-06-02 09:59:42 +02:00
1e4b4cad73 Expose document edits through pano cli 2026-06-02 09:56:12 +02:00
a6aa31da79 Expose document history through pano cli 2026-06-02 09:52:25 +02:00
b82cc1e4bd Expose recording renderer through pano cli 2026-06-02 09:47:09 +02:00
1d44036933 Add headless recording renderer api 2026-06-02 09:44:04 +02:00
61f86f5aae Add renderer boundary automation guard 2026-06-02 09:37:57 +02:00
acd8ef6658 Move Windows WGL context attributes to renderer gl 2026-06-02 09:32:27 +02:00
c22f2e7fa2 Route remaining canvas GL constants through renderer gl 2026-06-02 09:27:42 +02:00
b7d9dfbf31 Move canvas import export GL mappings to renderer gl 2026-06-02 09:24:37 +02:00
92338a0911 Move canvas layer merge GL mappings to renderer gl 2026-06-02 09:22:35 +02:00
f7d32f2835 Move canvas merge GL mappings to renderer gl 2026-06-02 09:20:18 +02:00
737c29cca4 Move canvas stroke commit GL mappings to renderer gl 2026-06-02 09:15:55 +02:00
37a59c01ac Move early canvas GL mappings to renderer gl 2026-06-02 09:12:39 +02:00
7ae37038b3 Move canvas resource formats to renderer gl 2026-06-02 09:09:22 +02:00
bbe8378630 Move NodeCanvas GL mappings to renderer gl 2026-06-02 09:04:28 +02:00
466c1d0cc0 Move canvas mode GL mappings to renderer gl 2026-06-02 09:00:53 +02:00
4a7eff24bf Move VR render state mapping to renderer gl 2026-06-02 08:58:07 +02:00
f968488e34 Move Windows GL info mapping to renderer gl 2026-06-02 08:54:51 +02:00
a12a3454c4 Move texture defaults behind renderer gl 2026-06-02 08:51:34 +02:00
8a92bc973b Move stroke preview render mapping to renderer gl 2026-06-02 08:47:27 +02:00
dbaf50cb6e Move GL state utility mapping to renderer gl 2026-06-02 08:43:42 +02:00
92e9de0441 Move grid panel render mapping to renderer gl 2026-06-02 08:39:54 +02:00
7280678593 Move canvas layer pixel mapping to renderer gl 2026-06-02 08:36:43 +02:00
b7c087617b Move simple UI blend mapping to renderer gl 2026-06-02 08:31:31 +02:00
d8e958769b Move color wheel state mapping to renderer gl 2026-06-02 08:28:24 +02:00
b85c530df7 Move image node state mapping to renderer gl 2026-06-02 08:24:58 +02:00
3823a612ae Move image texture node state mapping to renderer gl 2026-06-02 08:15:36 +02:00
2a3402e991 Move viewport render state mapping to renderer gl 2026-06-02 08:13:03 +02:00
bbb85bb133 Move canvas action pixel mapping to renderer gl 2026-06-02 08:08:49 +02:00
d0b0dc3865 Move font render mapping to renderer gl 2026-06-02 07:36:09 +02:00
6fc8b9e5d2 Move extension query mapping to renderer gl 2026-06-02 07:33:40 +02:00
217450e161 Move app render state mapping to renderer gl 2026-06-02 07:01:07 +02:00
3930f39b14 Move app command state mapping to renderer gl 2026-06-02 06:58:30 +02:00
19f815e3d2 Move app init state mapping to renderer gl 2026-06-02 06:54:43 +02:00
9e0a88726c Move shader state mapping to renderer gl 2026-06-02 06:51:31 +02:00
47eb1ec0b2 Move shape buffer mapping to renderer gl 2026-06-02 06:47:29 +02:00
0d2a1bd0ae Move texture cube mapping to renderer gl 2026-06-02 06:43:51 +02:00
85a5d19a3e Move texture2d mapping to renderer gl 2026-06-02 06:41:00 +02:00
02f14f1bf5 Move render target clear mapping to renderer gl 2026-06-02 06:38:32 +02:00
e00eec30d4 Move framebuffer setup mapping to renderer gl 2026-06-02 06:35:07 +02:00
43e3a74c42 Move pixel buffer mapping to renderer gl 2026-06-02 06:31:15 +02:00
75dfc85978 Move readback format mapping to renderer gl 2026-06-02 06:27:29 +02:00
9ce49ef19c Move framebuffer blit mapping to renderer gl 2026-06-02 06:25:14 +02:00
36fea6b870 Move sampler border parameter to renderer gl 2026-06-02 06:22:41 +02:00
8130a922d0 Move sampler parameter mapping to renderer gl 2026-06-01 18:13:55 +02:00
f1e2743d58 Move render target texture parameters to renderer gl 2026-06-01 18:11:44 +02:00
7d80afce2f Move cube face target mapping to renderer gl 2026-06-01 18:09:12 +02:00
4212387b70 Extract OpenGL shader uniform catalog 2026-06-01 18:06:14 +02:00
bdcd44b340 Extract OpenGL shader attribute bindings 2026-06-01 17:58:09 +02:00
05064b3a0d Move OpenGL shape draw mappings 2026-06-01 17:52:59 +02:00
aa32c47e18 Move OpenGL framebuffer diagnostics 2026-06-01 17:50:51 +02:00
2e0ebd0e13 Move OpenGL image format mapping 2026-06-01 17:48:30 +02:00
2754df9f46 Move OpenGL texture upload mapping 2026-06-01 17:46:48 +02:00
9ab73a0354 Add OpenGL renderer capability target 2026-06-01 17:44:00 +02:00
d61c7f37c3 Extract renderer shader catalog 2026-06-01 17:36:25 +02:00
ad255a6ddf Attach PPI pixels to documents 2026-06-01 13:43:27 +02:00
88507df90e Decode PPI face payloads 2026-06-01 13:35:03 +02:00
10e5d5b5ae Preserve per-layer document timelines 2026-06-01 13:05:14 +02:00
c16cab87bd Load PPI metadata into documents 2026-06-01 13:00:14 +02:00
7319cb9aa9 Index PPI project layers 2026-06-01 12:53:48 +02:00
677d0b33a8 Validate PPI face payload metadata 2026-06-01 12:48:55 +02:00
f1ee1b28a1 Summarize PPI project bodies 2026-06-01 12:44:49 +02:00
2da247f0fb Validate PPI project layout 2026-06-01 12:38:21 +02:00
37854ea8b9 Add paint stroke script automation 2026-06-01 12:34:15 +02:00
dc252b2f24 Simulate strokes from pano cli 2026-06-01 09:12:55 +02:00
d0ef88be89 Create animation documents from pano cli 2026-06-01 09:10:59 +02:00
4ec2d093e8 Fix clang-cl ASan preset setup 2026-06-01 09:09:16 +02:00
4eee018367 Validate renderer shader descriptors 2026-06-01 09:05:43 +02:00
44aebf61b2 Add document frame move coverage 2026-06-01 09:03:46 +02:00
f6d3de8cbf Expose PNG metadata in pano cli 2026-06-01 09:01:21 +02:00
c62bc4d744 Add assets PNG metadata tests 2026-06-01 08:58:28 +02:00
8ebb22325c Use vcpkg tinyxml2 in headless preset 2026-06-01 08:52:31 +02:00
e5d98c2dc3 Validate headless vcpkg preset 2026-06-01 08:49:37 +02:00
abe578a338 Add paint brush parameter tests 2026-06-01 08:40:46 +02:00
313a360c01 Add UI core color parser tests 2026-06-01 08:38:05 +02:00
551013c771 Add document layer metadata tests 2026-06-01 08:34:26 +02:00
cc377b5eb5 Add assets settings document tests 2026-06-01 08:32:29 +02:00
6c435dafb7 Add foundation event dispatcher tests 2026-06-01 08:28:57 +02:00
3f5711773e Add foundation task queue tests 2026-06-01 08:23:59 +02:00
a7bb04f54b Add foundation logging facade 2026-06-01 08:20:58 +02:00
6604f30ef3 Expand renderer API interfaces 2026-06-01 08:15:21 +02:00
93d8aaaffd Add paint stroke sampling tests 2026-06-01 08:08:27 +02:00
f9e4bcaeea Add shader validation automation hook 2026-06-01 07:55:39 +02:00
3d80791245 Add document undo history tests 2026-06-01 07:39:42 +02:00
126280ff7c Add PPI header recognition tests 2026-06-01 00:26:06 +02:00
20b5dba41e Add UI layout XML automation 2026-06-01 00:21:23 +02:00
dfdb7a4468 Add document animation frame tests 2026-06-01 00:16:34 +02:00
4d715afd60 Add paint renderer compositor tests 2026-06-01 00:13:53 +02:00
ac0d0ab49c Add package smoke automation 2026-06-01 00:09:34 +02:00
a67e7fc9bb Start UI core layout value tests 2026-06-01 00:07:55 +02:00
31322bbd83 Add renderer API validation tests 2026-06-01 00:05:41 +02:00
23eba07901 Start document model tests 2026-06-01 00:02:42 +02:00
8014345b99 Add paint blend reference tests 2026-05-31 23:58:47 +02:00
99eda95cee Start assets component image signature tests 2026-05-31 23:55:20 +02:00
ec5ecbdb54 Add foundation tracing and platform build wrapper 2026-05-31 23:51:41 +02:00
e0ea4597e6 Add Android headless preset and parser tests 2026-05-31 23:46:41 +02:00
c38ff8209b Start CMake modernization scaffold 2026-05-31 23:40:43 +02:00
363 changed files with 92173 additions and 5925 deletions

4
.gitignore vendored
View File

@@ -39,6 +39,8 @@ PanoPainterPackage/_pkginfo.txt
PanoPainterPackage/AppPackages/
PanoPainterPackage/BundleArtifacts/
Thumbs.db
__pycache__/
*.pyc
steam/content/
steam/output/
@@ -53,3 +55,5 @@ linux/Makefile
webgl/build
webgl/.vscode
out/

161
AGENTS.md Normal file
View File

@@ -0,0 +1,161 @@
# 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/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.
- 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.
## Build And Test
Primary Windows configure/build:
```powershell
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
```
Quiet checkpoint validation, preferred when working through Codex token-limited
sessions:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"
```
The quiet wrapper writes full command logs under `out/logs/quiet-validation`,
prints only a compact summary, and applies editable warning/noise filters from
`scripts/automation/quiet-validation-ignore.txt`. If a step fails, read the
reported log file instead of rerunning with verbose output.
Focused fast validation:
```powershell
ctest --preset desktop-fast --build-config Debug --output-on-failure
```
Useful targeted pattern:
```powershell
ctest --preset desktop-fast --build-config Debug -R "pp_app_core|pano_cli_plan" --output-on-failure
```
Apple compile validation runs through the Mac mini SSH alias `panopainter-mac`:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device
```
This is a headless component/test/tool compile gate for macOS, iOS simulator,
and iOS device. Signed Apple bundles remain tracked in the debt log.
If MSVC reports corrupt debug information or stale PDB/object errors, clean the
generated build tree target before judging source changes:
```powershell
cmake --build --preset windows-msvc-default --config Debug --target clean
```
## Code Navigation
Codex has a repo-specific skill named `panopainter-code-navigation`. Agents must
use it when necessary: follow this path before broad text search when a task is
about C++ symbol identity, symbol families, declarations/definitions, override
groups, or platform/backend boundaries. Use it when
following C++ symbols, finding symbol families with regular expressions, tracing
service/interface wiring, or checking platform/backend boundary usage.
Reach for `--name-regex`,
`--detail-regex`, or `--path-regex` when looking for generated-style name
families, override groups, command/service families, signatures, or
platform/backend path slices. Use normal `rg` for docs, build files, comments,
string literals, generated command names, or exact non-C++ text.
Prefer compiler-aware navigation when following C++ symbols across the legacy
flat source tree and extracted components:
```powershell
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
python scripts/dev/clangd_nav.py self-test
```
The helper talks to `clangd` using an existing `compile_commands.json`. It
defaults to `out/build/windows-clangcl-asan` and then `out/build/android-arm64`;
pass `--compile-commands-dir` or set `PP_CLANGD_COMPILE_COMMANDS_DIR` when using
another Ninja build tree. Use `--name` and `--max-results` to keep output small.
Use `--name-regex` for regex filtering against `qualifiedName`,
`--detail-regex` for symbol detail/signature filtering, and `--path-regex` for
definition/declaration/implementation/reference location filtering. Regex
matching is case-insensitive by default, and `--no-ignore-case` makes it
case-sensitive. Run `python scripts/dev/clangd_nav.py self-test` or the
`panopainter_clangd_nav_regex_self_test` CTest before relying on regex behavior
after tool changes.
Treat symbol, hover, declaration, definition, and implementation lookups as the
reliable path. Reference lookups are riskier because a one-shot clangd process
may not have a complete project index; the helper refuses reference queries
unless callers pass `--background-index` for broader best-effort results or
`--allow-incomplete-references` for explicitly current-translation-unit-only
results. Do not use incomplete reference output as proof that a symbol has no
other users.
## Current Architecture Direction
The desired component split is documented in the roadmap. Current extracted or
in-progress boundaries include:
- `pp_foundation`
- `pp_assets`
- `pp_paint`
- `pp_document`
- `pp_renderer_api`
- `pp_renderer_gl`
- `pp_paint_renderer`
- `pp_app_core`
- `pp_panopainter_ui`
- `pp_platform_*`
- `panopainter_app`
Legacy UI/app code still exists and is being reduced through pure planners,
service interfaces, CLI automation, and CTest coverage. When moving behavior out
of a legacy node, preserve current behavior first, add focused tests, document
remaining shortcuts in `docs/modernization/debt.md`, then wire the live adapter.
## Legacy Context
PanoPainter started as a flat C++17 OpenGL panoramic painting app centered on
singletons such as `App::I`, `Canvas::I`, `Settings`, and `WacomTablet::I`.
Rendering uses GLAD/OpenGL and shaders in `data/shaders/`. UI is XML/Yoga based
with many `Node*` subclasses. Project files are PPI, brush packages are PPBR,
and Photoshop brushes import through ABR support.
Exceptions are disabled in app code. Public modernization APIs should return
explicit status/result objects.

640
CMakeLists.txt Normal file
View File

@@ -0,0 +1,640 @@
cmake_minimum_required(VERSION 3.29)
project(PanoPainter
VERSION 0.0.0
DESCRIPTION "Panoramic painting and animation application"
LANGUAGES C CXX)
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW)
endif()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(PanoPainterOptions)
if(PP_ENABLE_ASAN AND MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
else()
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
include(PanoPainterWarnings)
include(PanoPainterSources)
include(PanoPainterVersion)
include(PanoPainterRuntime)
include(PanoPainterPackageTargets)
include(PanoPainterPlatformTargets)
if(PP_ENABLE_CLANG_TIDY)
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
if(PP_CLANG_TIDY_EXE)
set(CMAKE_CXX_CLANG_TIDY "${PP_CLANG_TIDY_EXE}")
else()
message(WARNING "PP_ENABLE_CLANG_TIDY is ON but clang-tidy was not found.")
endif()
endif()
if(PP_ENABLE_CPPCHECK)
find_program(PP_CPPCHECK_EXE NAMES cppcheck)
if(PP_CPPCHECK_EXE)
set(CMAKE_CXX_CPPCHECK
"${PP_CPPCHECK_EXE}"
"--enable=warning,style,performance,portability"
"--inline-suppr"
"--suppress=missingIncludeSystem")
else()
message(WARNING "PP_ENABLE_CPPCHECK is ON but cppcheck was not found.")
endif()
endif()
add_library(pp_project_options INTERFACE)
target_compile_features(pp_project_options INTERFACE cxx_std_23)
add_library(pp_project_warnings INTERFACE)
pp_configure_project_warnings(pp_project_warnings)
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER
"com.omigamedev.panopainter.$(PRODUCT_NAME:rfc1034identifier)")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO")
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
endif()
if(PP_USE_VCPKG_TINYXML2)
find_package(tinyxml2 CONFIG REQUIRED)
add_library(pp_xml_tinyxml2 INTERFACE)
target_link_libraries(pp_xml_tinyxml2
INTERFACE
tinyxml2::tinyxml2)
else()
add_library(pp_vendor_tinyxml2 STATIC
libs/tinyxml2/tinyxml2.cpp)
target_include_directories(pp_vendor_tinyxml2
SYSTEM PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2")
target_link_libraries(pp_vendor_tinyxml2
PUBLIC
pp_project_options)
add_library(pp_xml_tinyxml2 ALIAS pp_vendor_tinyxml2)
endif()
add_custom_target(panopainter_modernization_status
COMMAND "${CMAKE_COMMAND}" -E echo "PanoPainter modernization scaffold configured."
COMMAND "${CMAKE_COMMAND}" -E echo "Roadmap: docs/modernization/roadmap.md"
COMMAND "${CMAKE_COMMAND}" -E echo "Debt log: docs/modernization/debt.md"
VERBATIM)
add_custom_target(panopainter_validate_shaders
COMMAND "${CMAKE_COMMAND}"
"-DPP_SHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/data/shaders"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidatePanoPainterShaders.cmake"
VERBATIM)
add_library(pp_foundation STATIC
src/foundation/binary_stream.cpp
src/foundation/event.cpp
src/foundation/log.cpp
src/foundation/parse.cpp
src/foundation/task_queue.cpp
src/foundation/trace.cpp)
target_include_directories(pp_foundation
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_foundation
PUBLIC
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_assets STATIC
src/assets/brush_package.cpp
src/assets/image_format.cpp
src/assets/image_metadata.cpp
src/assets/image_pixels.cpp
src/assets/ppi_header.cpp
src/assets/settings_document.cpp)
target_include_directories(pp_assets
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_include_directories(pp_assets
SYSTEM PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb")
if(MSVC)
set_source_files_properties(src/assets/image_pixels.cpp
PROPERTIES
COMPILE_OPTIONS "/analyze-")
endif()
target_link_libraries(pp_assets
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_paint STATIC
src/paint/brush.cpp
src/paint/blend.cpp
src/paint/stroke.cpp
src/paint/stroke_script.cpp)
target_include_directories(pp_paint
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_paint
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_document STATIC
src/document/document.cpp
src/document/ppi_export.cpp
src/document/ppi_import.cpp)
target_include_directories(pp_document
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_document
PUBLIC
pp_foundation
pp_assets
pp_paint
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_renderer_api STATIC
src/renderer_api/recording_renderer.cpp
src/renderer_api/renderer_api.cpp
src/renderer_api/shader_catalog.cpp)
target_include_directories(pp_renderer_api
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_renderer_api
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_project_warnings)
if(PP_ENABLE_OPENGL)
add_library(pp_renderer_gl STATIC
src/renderer_gl/command_plan.cpp
src/renderer_gl/opengl_capabilities.cpp
src/renderer_gl/shader_bindings.cpp)
target_include_directories(pp_renderer_gl
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_renderer_gl
PUBLIC
pp_renderer_api
pp_project_options
PRIVATE
pp_project_warnings)
if(EMSCRIPTEN)
target_compile_definitions(pp_renderer_gl PRIVATE
PP_RENDERER_GL_RUNTIME_GLES=1
PP_RENDERER_GL_RUNTIME_WEB=1)
elseif(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
target_compile_definitions(pp_renderer_gl PRIVATE
PP_RENDERER_GL_RUNTIME_GLES=1)
else()
target_compile_definitions(pp_renderer_gl PRIVATE
PP_RENDERER_GL_RUNTIME_DESKTOP=1)
endif()
endif()
add_library(pp_paint_renderer STATIC
src/paint_renderer/compositor.cpp)
target_include_directories(pp_paint_renderer
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_paint_renderer
PUBLIC
pp_foundation
pp_document
pp_paint
pp_renderer_api
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_ui_core STATIC
src/ui_core/color.cpp
src/ui_core/layout_value.cpp
src/ui_core/layout_xml.cpp
src/ui_core/node_lifetime.cpp
src/ui_core/overlay_lifetime.cpp)
target_include_directories(pp_ui_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_ui_core
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_xml_tinyxml2
pp_project_warnings)
add_library(pp_platform_api STATIC
src/platform_api/asset_file_load_policy.cpp
src/platform_api/asset_file_load_policy.h
src/platform_api/network_tls_policy.cpp
src/platform_api/network_tls_policy.h
src/platform_api/platform_policy.cpp
src/platform_api/platform_policy.h
src/platform_api/platform_services.cpp
src/platform_api/platform_services.h)
target_include_directories(pp_platform_api
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_platform_api
PUBLIC
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_app_core STATIC
src/app_core/about_menu.h
src/app_core/app_dialog.h
src/app_core/app_frame.h
src/app_core/app_input.h
src/app_core/app_preferences.h
src/app_core/app_shutdown.h
src/app_core/app_status.h
src/app_core/app_startup.h
src/app_core/app_thread.h
src/app_core/brush_package_import.h
src/app_core/brush_package_export.h
src/app_core/brush_ui.h
src/app_core/canvas_hotkey.h
src/app_core/canvas_tool_ui.h
src/app_core/canvas_view.h
src/app_core/command_convert.h
src/app_core/document_animation.h
src/app_core/document_canvas.h
src/app_core/document_cloud.h
src/app_core/document_export.cpp
src/app_core/document_import.h
src/app_core/document_layer.h
src/app_core/document_platform_io.h
src/app_core/document_recording.h
src/app_core/document_resize.h
src/app_core/document_route.cpp
src/app_core/document_sharing.h
src/app_core/document_session.cpp
src/app_core/file_menu.h
src/app_core/grid_ui.h
src/app_core/history_ui.h
src/app_core/main_toolbar.h
src/app_core/quick_ui.h
src/app_core/tools_menu.h)
target_include_directories(pp_app_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_app_core
PUBLIC
pp_foundation
pp_document
pp_project_options
PRIVATE
pp_project_warnings)
if(PP_BUILD_TOOLS)
add_subdirectory(tools/pano_cli)
endif()
if(PP_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
if(PP_BUILD_APP)
if(WIN32)
add_library(pp_legacy_vendor OBJECT
${PP_VENDOR_SOURCES})
target_link_libraries(pp_legacy_vendor
PUBLIC
pp_project_options
PRIVATE
pp_project_warnings)
target_include_directories(pp_legacy_vendor
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
if(MSVC_VERSION GREATER_EQUAL 1945)
set(PP_FMT_VS2026_COMPAT_INCLUDE_DIR
"${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026/include")
set(PP_FMT_VS2026_COMPAT_FORMAT_H
"${PP_FMT_VS2026_COMPAT_INCLUDE_DIR}/fmt/format.h")
file(MAKE_DIRECTORY "${PP_FMT_VS2026_COMPAT_INCLUDE_DIR}")
file(COPY
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include/fmt"
DESTINATION "${PP_FMT_VS2026_COMPAT_INCLUDE_DIR}")
file(READ
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include/fmt/format.h"
PP_FMT_FORMAT_H)
# VS 2026 removed stdext::checked_array_iterator; keep the fmt
# submodule clean by disabling that legacy-only branch in the
# generated overlay.
string(REPLACE
"#ifdef _SECURE_SCL"
"#if 0"
PP_FMT_FORMAT_H
"${PP_FMT_FORMAT_H}")
file(WRITE "${PP_FMT_VS2026_COMPAT_FORMAT_H}" "${PP_FMT_FORMAT_H}")
target_include_directories(pp_legacy_vendor
BEFORE PRIVATE
"${PP_FMT_VS2026_COMPAT_INCLUDE_DIR}")
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)
set_target_properties(pp_legacy_vendor PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_library(pp_legacy_renderer_gl OBJECT
${PP_LEGACY_RENDERER_GL_SOURCES})
target_link_libraries(pp_legacy_renderer_gl
PUBLIC
pp_project_options
PRIVATE
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_renderer_gl PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_renderer_gl
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_renderer_gl
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_renderer_gl PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_renderer_gl PRIVATE src/pch.h)
add_library(pp_legacy_assets_io OBJECT
${PP_LEGACY_ASSETS_IO_SOURCES})
target_link_libraries(pp_legacy_assets_io
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_platform_api
pp_project_warnings)
target_include_directories(pp_legacy_assets_io
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_assets_io
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_assets_io PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_assets_io PRIVATE src/pch.h)
add_library(pp_legacy_paint_document OBJECT
${PP_LEGACY_PAINT_DOCUMENT_SOURCES})
target_link_libraries(pp_legacy_paint_document
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_document
pp_paint
pp_paint_renderer
pp_platform_api
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_paint_document PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_paint_document
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_paint_document
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_paint_document PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_paint_document PRIVATE src/pch.h)
add_library(pp_legacy_engine STATIC
${PP_LEGACY_ENGINE_SOURCES}
$<TARGET_OBJECTS:pp_legacy_assets_io>
$<TARGET_OBJECTS:pp_legacy_paint_document>
$<TARGET_OBJECTS:pp_legacy_renderer_gl>
$<TARGET_OBJECTS:pp_legacy_vendor>)
target_link_libraries(pp_legacy_engine
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_document
pp_paint
pp_paint_renderer
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_engine PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_engine
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_engine
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_engine PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_engine PRIVATE src/pch.h)
add_library(pp_legacy_ui_core OBJECT
${PP_LEGACY_UI_CORE_SOURCES})
target_link_libraries(pp_legacy_ui_core
PUBLIC
pp_app_core
pp_legacy_engine
pp_project_options
PRIVATE
pp_paint_renderer
pp_platform_api
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_ui_core PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_ui_core
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_ui_core
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_ui_core PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_ui_core PRIVATE src/pch.h)
add_library(pp_legacy_app STATIC
${PP_LEGACY_APP_SOURCES}
$<TARGET_OBJECTS:pp_legacy_ui_core>)
target_link_libraries(pp_legacy_app
PUBLIC
pp_legacy_engine
pp_legacy_ui_core
pp_project_options
PRIVATE
pp_platform_api
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_app
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_app
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_app PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
add_library(pp_panopainter_ui STATIC
${PP_PANOPAINTER_UI_SOURCES})
target_link_libraries(pp_panopainter_ui
PUBLIC
pp_legacy_app
pp_project_options
PRIVATE
pp_assets
pp_platform_api
pp_project_warnings)
target_precompile_headers(pp_panopainter_ui REUSE_FROM pp_legacy_app)
set_target_properties(pp_panopainter_ui PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_library(panopainter_app STATIC
${PP_PANOPAINTER_APP_SOURCES})
target_include_directories(panopainter_app
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(panopainter_app
PUBLIC
pp_app_core
pp_legacy_app
pp_panopainter_ui
pp_platform_api
pp_project_options
PRIVATE
pp_project_warnings)
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
add_library(pp_platform_windows OBJECT
${PP_WINDOWS_PLATFORM_SOURCES})
target_link_libraries(pp_platform_windows
PUBLIC
panopainter_app
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/yuv.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/lib/win64/openvr_api.lib"
comdlg32
gdi32
opengl32
ole32
shell32
shlwapi
user32
wbemuuid
PRIVATE
pp_project_options
pp_project_warnings)
target_precompile_headers(pp_platform_windows REUSE_FROM pp_legacy_app)
set_target_properties(pp_platform_windows PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_executable(PanoPainter WIN32
${PP_WINDOWS_APP_SOURCES}
$<TARGET_OBJECTS:pp_platform_windows>)
target_link_libraries(PanoPainter
PRIVATE
pp_project_options
pp_project_warnings
pp_platform_windows)
set_target_properties(PanoPainter PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
pp_configure_windows_runtime_payloads(PanoPainter)
else()
message(WARNING "PP_BUILD_APP is enabled, but the root CMake app target is currently Windows-only. Platform alignment is tracked in Phase 6.")
endif()
endif()

297
CMakePresets.json Normal file
View File

@@ -0,0 +1,297 @@
{
"version": 8,
"cmakeMinimumRequired": {
"major": 3,
"minor": 29,
"patch": 0
},
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "${sourceDir}/out/build/${presetName}",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"PP_BUILD_APP": "ON",
"PP_BUILD_TESTS": "ON",
"PP_BUILD_TOOLS": "ON",
"PP_ENABLE_OPENGL": "ON",
"PP_ENABLE_VULKAN_EXPERIMENTAL": "OFF",
"PP_ENABLE_VR": "ON",
"PP_ENABLE_CLOUD": "ON",
"PP_ENABLE_VIDEO": "ON"
}
},
{
"name": "platform-headless-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"PP_BUILD_APP": "OFF",
"PP_ENABLE_CLOUD": "OFF",
"PP_ENABLE_VIDEO": "OFF"
}
},
{
"name": "windows-vs2026-x64",
"inherits": "base",
"displayName": "Windows VS 2026 x64",
"generator": "Visual Studio 18 2026",
"architecture": "x64"
},
{
"name": "windows-msvc-default",
"inherits": "base",
"displayName": "Windows MSVC default generator",
"generator": "Visual Studio 18 2026",
"architecture": "x64"
},
{
"name": "windows-msvc-vcpkg-headless",
"inherits": "platform-headless-base",
"displayName": "Windows MSVC vcpkg headless",
"generator": "Visual Studio 18 2026",
"architecture": "x64",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"PP_USE_VCPKG_TINYXML2": "ON"
}
},
{
"name": "windows-clangcl-asan",
"inherits": "platform-headless-base",
"displayName": "Windows clang-cl ASan",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang-cl",
"CMAKE_CXX_COMPILER": "clang-cl",
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL",
"PP_ENABLE_ASAN": "ON",
"PP_ENABLE_UBSAN": "OFF"
}
},
{
"name": "linux-clang",
"inherits": "platform-headless-base",
"displayName": "Linux clang",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
}
},
{
"name": "android-base",
"hidden": true,
"inherits": "platform-headless-base",
"generator": "Ninja",
"toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
"cacheVariables": {
"ANDROID_PLATFORM": "android-26",
"ANDROID_STL": "c++_shared",
"PP_ENABLE_VR": "OFF",
"PP_ENABLE_OPENGL": "ON"
}
},
{
"name": "android-arm64",
"inherits": "android-base",
"displayName": "Android arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a"
}
},
{
"name": "android-x64",
"inherits": "android-base",
"displayName": "Android x86_64",
"cacheVariables": {
"ANDROID_ABI": "x86_64"
}
},
{
"name": "android-quest-arm64",
"inherits": "android-base",
"displayName": "Android Quest arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"PP_ENABLE_VR": "ON",
"PP_ANDROID_FLAVOR": "quest"
}
},
{
"name": "android-focus-arm64",
"inherits": "android-base",
"displayName": "Android Focus/Wave arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"PP_ENABLE_VR": "ON",
"PP_ANDROID_FLAVOR": "focus"
}
},
{
"name": "emscripten",
"inherits": "platform-headless-base",
"displayName": "Emscripten WebGL",
"generator": "Ninja",
"cacheVariables": {
"PP_ENABLE_VR": "OFF",
"PP_ENABLE_VIDEO": "OFF"
}
},
{
"name": "macos",
"inherits": "platform-headless-base",
"displayName": "macOS",
"generator": "Ninja"
},
{
"name": "ios-device",
"inherits": "platform-headless-base",
"displayName": "iOS device",
"generator": "Xcode",
"cacheVariables": {
"CMAKE_SYSTEM_NAME": "iOS",
"CMAKE_OSX_SYSROOT": "iphoneos"
}
},
{
"name": "ios-simulator",
"inherits": "platform-headless-base",
"displayName": "iOS simulator",
"generator": "Xcode",
"cacheVariables": {
"CMAKE_SYSTEM_NAME": "iOS",
"CMAKE_OSX_SYSROOT": "iphonesimulator"
}
}
],
"buildPresets": [
{
"name": "windows-vs2026-x64",
"configurePreset": "windows-vs2026-x64"
},
{
"name": "windows-msvc-default",
"configurePreset": "windows-msvc-default"
},
{
"name": "windows-msvc-vcpkg-headless",
"configurePreset": "windows-msvc-vcpkg-headless"
},
{
"name": "windows-clangcl-asan",
"configurePreset": "windows-clangcl-asan"
},
{
"name": "linux-clang",
"configurePreset": "linux-clang"
},
{
"name": "android-arm64",
"configurePreset": "android-arm64"
},
{
"name": "android-x64",
"configurePreset": "android-x64"
},
{
"name": "android-quest-arm64",
"configurePreset": "android-quest-arm64"
},
{
"name": "android-focus-arm64",
"configurePreset": "android-focus-arm64"
},
{
"name": "emscripten",
"configurePreset": "emscripten"
},
{
"name": "macos",
"configurePreset": "macos"
},
{
"name": "ios-device",
"configurePreset": "ios-device"
},
{
"name": "ios-simulator",
"configurePreset": "ios-simulator"
}
],
"testPresets": [
{
"name": "desktop-fast",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"exclude": {
"label": "gpu|slow|platform"
}
}
},
{
"name": "desktop-fast-vs2026",
"configurePreset": "windows-vs2026-x64",
"output": {
"outputOnFailure": true
},
"filter": {
"exclude": {
"label": "gpu|slow|platform"
}
}
},
{
"name": "desktop-fast-vcpkg",
"configurePreset": "windows-msvc-vcpkg-headless",
"output": {
"outputOnFailure": true
},
"filter": {
"exclude": {
"label": "gpu|slow|platform"
}
}
},
{
"name": "desktop-gpu",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"include": {
"label": "gpu"
}
}
},
{
"name": "fuzz",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"include": {
"label": "fuzz"
}
}
},
{
"name": "stress",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"include": {
"label": "stress"
}
}
}
]
}

View File

@@ -1,57 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2026
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PanoPainter", "PanoPainter.vcxproj", "{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PanoPainterPackage", "PanoPainterPackage\PanoPainterPackage.wapproj", "{3A716FB6-DE62-439F-83B6-3C40915D6678}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|Any CPU.ActiveCfg = Debug|Win32
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.ActiveCfg = Debug|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Build.0 = Debug|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x64.Deploy.0 = Debug|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.ActiveCfg = Debug|Win32
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Debug|x86.Build.0 = Debug|Win32
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|Any CPU.ActiveCfg = Release|Win32
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.ActiveCfg = Release|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Build.0 = Release|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x64.Deploy.0 = Release|x64
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.ActiveCfg = Release|Win32
{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}.Release|x86.Build.0 = Release|Win32
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.ActiveCfg = Debug|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Build.0 = Debug|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x64.Deploy.0 = Debug|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.ActiveCfg = Debug|x86
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Build.0 = Debug|x86
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Debug|x86.Deploy.0 = Debug|x86
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Build.0 = Release|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|Any CPU.Deploy.0 = Release|Any CPU
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.ActiveCfg = Release|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Build.0 = Release|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x64.Deploy.0 = Release|x64
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.ActiveCfg = Release|x86
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Build.0 = Release|x86
{3A716FB6-DE62-439F-83B6-3C40915D6678}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8EFC4B-CEA1-4408-8628-7D2C0F6C43C8}
EndGlobalSection
EndGlobal

View File

@@ -1,634 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{6D5028CE-4D76-4B6A-A7C2-DE5A3268D433}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>PanoPainter</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
<ProjectName>PanoPainter</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\source\Client;$(IncludePath)</IncludePath>
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);C:\Users\omar\Downloads\BugTrap-master\BugTrap-master\bin;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<IncludePath>libs\glm;libs\glew-2.0.0\include;libs\stb;libs\tinyxml2;libs\yoga;libs\curl-win\include;libs\jpeg;libs\wacom;libs\bugtrap-client\include;libs\poly2tri\poly2tri;libs\base64;libs\sqlite3;libs\openvr\headers;libs\nanort;libs\hash-library;libs\fmt\include;libs\glad\include;libs\openh264\include;libs\mp4v2\include;libs\libyuv\include;C:\Program Files\RenderDoc;$(IncludePath)</IncludePath>
<LibraryPath>libs\curl-win\lib\dll-$(Configuration)-$(PlatformShortName);libs\glew-2.0.0\lib\Release\$(Platform);libs\bugtrap-client\lib;libs\openvr\lib\win64;libs\openh264\lib;libs\mp4v2\lib\win;libs\libyuv\lib\win;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;DEBUG;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<ExceptionHandling>false</ExceptionHandling>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<PreBuildEvent>
<Command>python .\scripts\pre-build.py debug</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<PrecompiledHeader>Use</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<PrecompiledHeader>Use</PrecompiledHeader>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>ENUM_BITFIELDS_NOT_SUPPORTED;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<ExceptionHandling>false</ExceptionHandling>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<PreBuildEvent>
<Command>python .\scripts\pre-build.py release</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="libs\fmt\src\format.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\fmt\src\posix.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\glad\src\glad.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\glad\src\glad_wgl.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\hash-library\md5.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\nanort\nanort.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\sqlite3\sqlite3.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\log.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
<AssemblerListingLocation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</AssemblerListingLocation>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)yoga\</XMLDocumentationFileName>
</ClCompile>
<ClCompile Include="src\abr.cpp" />
<ClCompile Include="src\action.cpp" />
<ClCompile Include="src\app.cpp" />
<ClCompile Include="src\app_cloud.cpp" />
<ClCompile Include="src\app_commands.cpp" />
<ClCompile Include="src\app_dialogs.cpp" />
<ClCompile Include="src\app_events.cpp" />
<ClCompile Include="src\app_layout.cpp" />
<ClCompile Include="src\app_shaders.cpp" />
<ClCompile Include="src\app_vr.cpp" />
<ClCompile Include="src\asset.cpp" />
<ClCompile Include="src\bezier.cpp" />
<ClCompile Include="src\binary_stream.cpp" />
<ClCompile Include="src\brush.cpp" />
<ClCompile Include="src\canvas.cpp" />
<ClCompile Include="src\canvas_actions.cpp" />
<ClCompile Include="src\canvas_layer.cpp" />
<ClCompile Include="src\canvas_modes.cpp" />
<ClCompile Include="src\event.cpp" />
<ClCompile Include="src\font.cpp" />
<ClCompile Include="src\hmd.cpp" />
<ClCompile Include="src\image.cpp" />
<ClCompile Include="src\layout.cpp" />
<ClCompile Include="src\log.cpp" />
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\mp4enc.cpp" />
<ClCompile Include="src\node.cpp" />
<ClCompile Include="src\node_about.cpp" />
<ClCompile Include="src\node_border.cpp" />
<ClCompile Include="src\node_button.cpp" />
<ClCompile Include="src\node_button_custom.cpp" />
<ClCompile Include="src\node_canvas.cpp" />
<ClCompile Include="src\node_changelog.cpp" />
<ClCompile Include="src\node_checkbox.cpp" />
<ClCompile Include="src\node_colorwheel.cpp" />
<ClCompile Include="src\node_color_quad.cpp" />
<ClCompile Include="src\node_combobox.cpp" />
<ClCompile Include="src\node_dialog_browse.cpp" />
<ClCompile Include="src\node_dialog_cloud.cpp" />
<ClCompile Include="src\node_dialog_export_ppbr.cpp" />
<ClCompile Include="src\node_dialog_layer_rename.cpp" />
<ClCompile Include="src\node_dialog_open.cpp" />
<ClCompile Include="src\node_dialog_picker.cpp" />
<ClCompile Include="src\node_dialog_resize.cpp" />
<ClCompile Include="src\node_icon.cpp" />
<ClCompile Include="src\node_image.cpp" />
<ClCompile Include="src\node_image_texture.cpp" />
<ClCompile Include="src\node_input_box.cpp" />
<ClCompile Include="src\node_message_box.cpp" />
<ClCompile Include="src\node_metadata.cpp" />
<ClCompile Include="src\node_panel_brush.cpp" />
<ClCompile Include="src\node_panel_color.cpp" />
<ClCompile Include="src\node_panel_floating.cpp" />
<ClCompile Include="src\node_panel_grid.cpp" />
<ClCompile Include="src\node_panel_layer.cpp" />
<ClCompile Include="src\node_panel_quick.cpp" />
<ClCompile Include="src\node_panel_stroke.cpp" />
<ClCompile Include="src\node_panel_animation.cpp" />
<ClCompile Include="src\node_popup_menu.cpp" />
<ClCompile Include="src\node_progress_bar.cpp" />
<ClCompile Include="src\node_remote_page.cpp" />
<ClCompile Include="src\node_scroll.cpp" />
<ClCompile Include="src\node_settings.cpp" />
<ClCompile Include="src\node_shorcuts.cpp" />
<ClCompile Include="src\node_slider.cpp" />
<ClCompile Include="src\node_stroke_preview.cpp" />
<ClCompile Include="src\node_text.cpp" />
<ClCompile Include="src\node_text_input.cpp" />
<ClCompile Include="src\node_tool_bucket.cpp" />
<ClCompile Include="src\node_usermanual.cpp" />
<ClCompile Include="src\node_viewport.cpp" />
<ClCompile Include="src\pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="src\rtt.cpp" />
<ClCompile Include="src\serializer.cpp" />
<ClCompile Include="src\settings.cpp" />
<ClCompile Include="src\shader.cpp" />
<ClCompile Include="src\shape.cpp" />
<ClCompile Include="src\texture.cpp" />
<ClCompile Include="src\util.cpp" />
<ClCompile Include="src\version.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="src\wacom.cpp" />
<ClCompile Include="libs\jpeg\jpgd.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\jpeg\jpge.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="libs\hash-library\md5.h" />
<ClInclude Include="libs\nanort\nanort.h" />
<ClInclude Include="libs\sqlite3\sqlite3.h" />
<ClInclude Include="libs\sqlite3\sqlite3ext.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="src\abr.h" />
<ClInclude Include="src\action.h" />
<ClInclude Include="src\app.h" />
<ClInclude Include="src\asset.h" />
<ClInclude Include="src\bezier.h" />
<ClInclude Include="src\binary_stream.h" />
<ClInclude Include="src\brush.h" />
<ClInclude Include="src\canvas.h" />
<ClInclude Include="src\canvas_actions.h" />
<ClInclude Include="src\canvas_layer.h" />
<ClInclude Include="src\canvas_modes.h" />
<ClInclude Include="src\event.h" />
<ClInclude Include="src\font.h" />
<ClInclude Include="src\hmd.h" />
<ClInclude Include="src\image.h" />
<ClInclude Include="src\keymap.h" />
<ClInclude Include="src\layout.h" />
<ClInclude Include="src\log.h" />
<ClInclude Include="src\mp4enc.h" />
<ClInclude Include="src\node.h" />
<ClInclude Include="src\node_about.h" />
<ClInclude Include="src\node_border.h" />
<ClInclude Include="src\node_button.h" />
<ClInclude Include="src\node_button_custom.h" />
<ClInclude Include="src\node_canvas.h" />
<ClInclude Include="src\node_changelog.h" />
<ClInclude Include="src\node_checkbox.h" />
<ClInclude Include="src\node_colorwheel.h" />
<ClInclude Include="src\node_color_quad.h" />
<ClInclude Include="src\node_combobox.h" />
<ClInclude Include="src\node_dialog_browse.h" />
<ClInclude Include="src\node_dialog_cloud.h" />
<ClInclude Include="src\node_dialog_export_ppbr.h" />
<ClInclude Include="src\node_dialog_layer_rename.h" />
<ClInclude Include="src\node_dialog_open.h" />
<ClInclude Include="src\node_dialog_picker.h" />
<ClInclude Include="src\node_dialog_resize.h" />
<ClInclude Include="src\node_icon.h" />
<ClInclude Include="src\node_image.h" />
<ClInclude Include="src\node_image_texture.h" />
<ClInclude Include="src\node_input_box.h" />
<ClInclude Include="src\node_message_box.h" />
<ClInclude Include="src\node_metadata.h" />
<ClInclude Include="src\node_panel_brush.h" />
<ClInclude Include="src\node_panel_color.h" />
<ClInclude Include="src\node_panel_floating.h" />
<ClInclude Include="src\node_panel_grid.h" />
<ClInclude Include="src\node_panel_layer.h" />
<ClInclude Include="src\node_panel_quick.h" />
<ClInclude Include="src\node_panel_stroke.h" />
<ClInclude Include="src\node_panel_animation.h" />
<ClInclude Include="src\node_popup_menu.h" />
<ClInclude Include="src\node_progress_bar.h" />
<ClInclude Include="src\node_remote_page.h" />
<ClInclude Include="src\node_scroll.h" />
<ClInclude Include="src\node_settings.h" />
<ClInclude Include="src\node_shorcuts.h" />
<ClInclude Include="src\node_slider.h" />
<ClInclude Include="src\node_stroke_preview.h" />
<ClInclude Include="src\node_text.h" />
<ClInclude Include="src\node_text_input.h" />
<ClInclude Include="src\node_tool_bucket.h" />
<ClInclude Include="src\node_usermanual.h" />
<ClInclude Include="src\node_viewport.h" />
<ClInclude Include="src\pch.h" />
<ClInclude Include="src\rtt.h" />
<ClInclude Include="src\serializer.h" />
<ClInclude Include="src\settings.h" />
<ClInclude Include="src\shader.h" />
<ClInclude Include="src\shape.h" />
<ClInclude Include="src\texture.h" />
<ClInclude Include="src\util.h" />
<ClInclude Include="src\version.gen.h" />
<ClInclude Include="src\version.h" />
<ClInclude Include="src\wacom.h" />
<ClInclude Include="libs\jpeg\jpgd.h" />
<ClInclude Include="libs\jpeg\jpge.h" />
<ClInclude Include="libs\tinyxml2\tinyxml2.h" />
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H" />
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H" />
<ClInclude Include="libs\wacom\WinTab\Utils.h" />
<ClInclude Include="libs\wacom\WinTab\WINTAB.H" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PanoPainter.rc" />
</ItemGroup>
<ItemGroup>
<Natvis Include="libs\glm\util\glm.natvis" />
</ItemGroup>
<ItemGroup>
<Xml Include="data\dialogs\about.xml" />
<Xml Include="data\dialogs\brush-export.xml" />
<Xml Include="data\dialogs\changelog.xml" />
<Xml Include="data\dialogs\cloud-browse.xml" />
<Xml Include="data\dialogs\color-picker.xml" />
<Xml Include="data\dialogs\doc-browse.xml" />
<Xml Include="data\dialogs\doc-new.xml" />
<Xml Include="data\dialogs\doc-open.xml" />
<Xml Include="data\dialogs\doc-resize.xml" />
<Xml Include="data\dialogs\doc-save.xml" />
<Xml Include="data\dialogs\input-box.xml" />
<Xml Include="data\dialogs\layer-rename.xml" />
<Xml Include="data\dialogs\message-box.xml" />
<Xml Include="data\dialogs\panel-animation.xml" />
<Xml Include="data\dialogs\panel-floating.xml" />
<Xml Include="data\dialogs\panel-grid.xml" />
<Xml Include="data\dialogs\panel-layers.xml" />
<Xml Include="data\dialogs\panel-brushes.xml" />
<Xml Include="data\dialogs\panel-presets.xml" />
<Xml Include="data\dialogs\panel-quick.xml" />
<Xml Include="data\dialogs\panel-stroke.xml" />
<Xml Include="data\dialogs\progress-bar.xml" />
<Xml Include="data\dialogs\remote-page.xml" />
<Xml Include="data\dialogs\settings.xml" />
<Xml Include="data\dialogs\shortcuts.xml" />
<Xml Include="data\dialogs\usermanual.xml" />
<Xml Include="data\layout.xml">
<SubType>Designer</SubType>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
</DeploymentContent>
</Xml>
</ItemGroup>
<ItemGroup>
<None Include="data\shaders\atlas.glsl" />
<None Include="data\shaders\bake-uv.glsl" />
<None Include="data\shaders\checkerboard.glsl" />
<None Include="data\shaders\color-hue.glsl" />
<None Include="data\shaders\color-quad.glsl" />
<None Include="data\shaders\color-tri.glsl" />
<None Include="data\shaders\color.glsl" />
<None Include="data\shaders\comp-draw.glsl" />
<None Include="data\shaders\comp-erase.glsl" />
<None Include="data\shaders\equirect.glsl" />
<None Include="data\shaders\font.glsl" />
<None Include="data\shaders\include\blend-stroke.glsl" />
<None Include="data\shaders\include\blend.glsl" />
<None Include="data\shaders\include\blur.glsl" />
<None Include="data\shaders\include\color.glsl" />
<None Include="data\shaders\include\ext-fb-fetch.glsl" />
<None Include="data\shaders\include\hsv.glsl" />
<None Include="data\shaders\include\rand.glsl" />
<None Include="data\shaders\lambert.glsl" />
<None Include="data\shaders\lightmap.glsl" />
<None Include="data\shaders\stroke-dilate.glsl" />
<None Include="data\shaders\stroke-instanced.glsl" />
<None Include="data\shaders\stroke-pad.glsl" />
<None Include="data\shaders\stroke-preview.glsl" />
<None Include="data\shaders\stroke.glsl" />
<None Include="data\shaders\texture-alpha.glsl" />
<None Include="data\shaders\texture-blend.glsl" />
<None Include="data\shaders\texture-colorize.glsl" />
<None Include="data\shaders\texture-mask.glsl" />
<None Include="data\shaders\texture.glsl" />
<None Include="data\shaders\uvs.glsl" />
<None Include="data\shaders\vertex-color.glsl" />
</ItemGroup>
<ItemGroup>
<Image Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<Xsd Include="extra\layout.xsd">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</Xsd>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -1,854 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Source Files\ui">
<UniqueIdentifier>{600b8daa-4234-4c37-b4ba-c22cad7d1dc3}</UniqueIdentifier>
</Filter>
<Filter Include="libs">
<UniqueIdentifier>{6d64b115-02d1-43e0-86c8-c8212f51162d}</UniqueIdentifier>
</Filter>
<Filter Include="libs\jpeg">
<UniqueIdentifier>{dc178d53-6a6d-4a18-a93c-d4994340515f}</UniqueIdentifier>
</Filter>
<Filter Include="libs\WinTab">
<UniqueIdentifier>{54dc9f46-d2e0-466c-90d2-eb5d72d5799d}</UniqueIdentifier>
</Filter>
<Filter Include="libs\yoga">
<UniqueIdentifier>{a4a12057-835e-47ff-be4d-ce58b36cecf5}</UniqueIdentifier>
</Filter>
<Filter Include="libs\tinyxml2">
<UniqueIdentifier>{6fe315aa-e2b9-4f01-8291-683a5fda123b}</UniqueIdentifier>
</Filter>
<Filter Include="libs\poly2tri">
<UniqueIdentifier>{bda6fa93-a186-41ca-9bd9-49b7e0fd1ca4}</UniqueIdentifier>
</Filter>
<Filter Include="extras">
<UniqueIdentifier>{e631ac80-1b9b-424f-8adf-e2bab71a566d}</UniqueIdentifier>
</Filter>
<Filter Include="libs\sqlite3">
<UniqueIdentifier>{ef44d179-f28b-458c-b3df-be2895553149}</UniqueIdentifier>
</Filter>
<Filter Include="libs\nanort">
<UniqueIdentifier>{be0c0053-abd8-4e2d-a294-7c54511b05a6}</UniqueIdentifier>
</Filter>
<Filter Include="libs\hash">
<UniqueIdentifier>{2a784067-6741-47a3-b668-cc45f2224286}</UniqueIdentifier>
</Filter>
<Filter Include="libs\fmt">
<UniqueIdentifier>{7b4f5b47-7a8b-4e4c-9e82-399bb5047ffc}</UniqueIdentifier>
</Filter>
<Filter Include="shaders">
<UniqueIdentifier>{b55fb692-a845-4ef2-9b0e-5b2dd8bd125f}</UniqueIdentifier>
</Filter>
<Filter Include="shaders\include">
<UniqueIdentifier>{a2cacb13-2854-44ee-9511-6cb8ac587428}</UniqueIdentifier>
</Filter>
<Filter Include="libs\glad">
<UniqueIdentifier>{ca37521b-213f-4bcf-acfd-eda1483a30b2}</UniqueIdentifier>
</Filter>
<Filter Include="extras\dialogs">
<UniqueIdentifier>{5ecb54ed-7c3d-46fd-9b5d-227abdbc5954}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\app.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\image.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\shader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\shape.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\texture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\font.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\util.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\asset.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\rtt.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\bezier.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\canvas.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\brush.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\log.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\action.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\event.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\canvas_modes.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_border.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_button.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_button_custom.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_canvas.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_checkbox.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_color_quad.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_open.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_icon.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_image.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_image_texture.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_message_box.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_brush.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_color.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_layer.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_stroke.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_popup_menu.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_settings.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_slider.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_stroke_preview.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_text.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_text_input.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_viewport.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\layout.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_scroll.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\app_shaders.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\app_layout.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\app_events.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\jpeg\jpgd.cpp">
<Filter>libs\jpeg</Filter>
</ClCompile>
<ClCompile Include="libs\jpeg\jpge.cpp">
<Filter>libs\jpeg</Filter>
</ClCompile>
<ClCompile Include="libs\tinyxml2\tinyxml2.cpp">
<Filter>libs\tinyxml2</Filter>
</ClCompile>
<ClCompile Include="src\wacom.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\wacom\WinTab\Utils.cpp">
<Filter>libs\WinTab</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_layer_rename.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\app_dialogs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\common\shapes.cc">
<Filter>libs\poly2tri</Filter>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\advancing_front.cc">
<Filter>libs\poly2tri</Filter>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\cdt.cc">
<Filter>libs\poly2tri</Filter>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep.cc">
<Filter>libs\poly2tri</Filter>
</ClCompile>
<ClCompile Include="libs\poly2tri\poly2tri\sweep\sweep_context.cc">
<Filter>libs\poly2tri</Filter>
</ClCompile>
<ClCompile Include="src\node_progress_bar.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_browse.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\app_commands.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_cloud.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\app_cloud.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node_combobox.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_picker.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_colorwheel.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_grid.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\version.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node_about.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_changelog.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_usermanual.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_resize.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="libs\sqlite3\sqlite3.c">
<Filter>libs\sqlite3</Filter>
</ClCompile>
<ClCompile Include="src\hmd.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\app_vr.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\nanort\nanort.cc">
<Filter>libs\nanort</Filter>
</ClCompile>
<ClCompile Include="libs\hash-library\md5.cpp">
<Filter>libs\hash</Filter>
</ClCompile>
<ClCompile Include="src\abr.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\fmt\src\format.cc">
<Filter>libs\fmt</Filter>
</ClCompile>
<ClCompile Include="libs\fmt\src\posix.cc">
<Filter>libs\fmt</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_quick.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\binary_stream.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\serializer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGConfig.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGEnums.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGLayout.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGNode.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGNodePrint.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGStyle.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\YGValue.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\Yoga.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\log.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\Utils.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_floating.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\settings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\canvas_actions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\canvas_layer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node_tool_bucket.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="libs\glad\src\glad.c">
<Filter>libs\glad</Filter>
</ClCompile>
<ClCompile Include="libs\glad\src\glad_wgl.c">
<Filter>libs\glad</Filter>
</ClCompile>
<ClCompile Include="src\node_dialog_export_ppbr.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_input_box.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_panel_animation.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\internal\experiments.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="libs\yoga\yoga\event\event.cpp">
<Filter>libs\yoga</Filter>
</ClCompile>
<ClCompile Include="src\mp4enc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\node_remote_page.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_metadata.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
<ClCompile Include="src\node_shorcuts.cpp">
<Filter>Source Files\ui</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="libs\jpeg\jpgd.h">
<Filter>libs\jpeg</Filter>
</ClInclude>
<ClInclude Include="libs\jpeg\jpge.h">
<Filter>libs\jpeg</Filter>
</ClInclude>
<ClInclude Include="libs\tinyxml2\tinyxml2.h">
<Filter>libs\tinyxml2</Filter>
</ClInclude>
<ClInclude Include="libs\wacom\WinTab\PKTDEF.H">
<Filter>libs\WinTab</Filter>
</ClInclude>
<ClInclude Include="libs\wacom\WinTab\Utils.h">
<Filter>libs\WinTab</Filter>
</ClInclude>
<ClInclude Include="libs\wacom\WinTab\WINTAB.H">
<Filter>libs\WinTab</Filter>
</ClInclude>
<ClInclude Include="libs\wacom\WinTab\MSGPACK.H">
<Filter>libs\WinTab</Filter>
</ClInclude>
<ClInclude Include="libs\sqlite3\sqlite3.h">
<Filter>libs\sqlite3</Filter>
</ClInclude>
<ClInclude Include="libs\sqlite3\sqlite3ext.h">
<Filter>libs\sqlite3</Filter>
</ClInclude>
<ClInclude Include="libs\nanort\nanort.h">
<Filter>libs\nanort</Filter>
</ClInclude>
<ClInclude Include="libs\hash-library\md5.h">
<Filter>libs\hash</Filter>
</ClInclude>
<ClInclude Include="src\abr.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\action.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\app.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\asset.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\bezier.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\binary_stream.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\brush.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\canvas.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\canvas_actions.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\canvas_layer.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\canvas_modes.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\event.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\font.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\hmd.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\image.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\keymap.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\log.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\pch.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\rtt.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\serializer.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\settings.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\shader.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\shape.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\texture.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\util.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\version.gen.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\version.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\wacom.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="src\layout.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_about.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_border.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_button.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_button_custom.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_canvas.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_changelog.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_checkbox.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_color_quad.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_colorwheel.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_combobox.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_browse.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_cloud.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_export_ppbr.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_layer_rename.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_open.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_picker.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_dialog_resize.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_icon.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_image.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_image_texture.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_input_box.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_message_box.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_brush.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_color.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_floating.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_grid.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_layer.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_quick.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_stroke.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_popup_menu.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_progress_bar.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_scroll.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_settings.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_slider.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_stroke_preview.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_text.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_text_input.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_tool_bucket.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_usermanual.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_viewport.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_panel_animation.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_remote_page.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_metadata.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\node_shorcuts.h">
<Filter>Source Files\ui</Filter>
</ClInclude>
<ClInclude Include="src\mp4enc.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PanoPainter.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Natvis Include="libs\glm\util\glm.natvis">
<Filter>extras</Filter>
</Natvis>
</ItemGroup>
<ItemGroup>
<Xml Include="data\layout.xml">
<Filter>extras</Filter>
</Xml>
<Xml Include="data\dialogs\changelog.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\about.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\usermanual.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\brush-export.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-layers.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-brushes.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-stroke.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-grid.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-quick.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\color-picker.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\input-box.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\message-box.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\progress-bar.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\layer-rename.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\doc-resize.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\doc-browse.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\doc-new.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\doc-save.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\cloud-browse.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\settings.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\doc-open.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-floating.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-presets.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\panel-animation.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\remote-page.xml">
<Filter>extras\dialogs</Filter>
</Xml>
<Xml Include="data\dialogs\shortcuts.xml">
<Filter>extras\dialogs</Filter>
</Xml>
</ItemGroup>
<ItemGroup>
<None Include="data\shaders\texture.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\include\blend-stroke.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\blur.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\color.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\ext-fb-fetch.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\hsv.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\rand.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\include\blend.glsl">
<Filter>shaders\include</Filter>
</None>
<None Include="data\shaders\comp-draw.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\comp-erase.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\equirect.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\font.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\lambert.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\lightmap.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\stroke.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\stroke-instanced.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\stroke-preview.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\texture-alpha.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\texture-blend.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\uvs.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\vertex-color.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\atlas.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\bake-uv.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\checkerboard.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\color.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\color-hue.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\color-quad.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\color-tri.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\texture-colorize.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\texture-mask.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\stroke-dilate.glsl">
<Filter>shaders</Filter>
</None>
<None Include="data\shaders\stroke-pad.glsl">
<Filter>shaders</Filter>
</None>
</ItemGroup>
<ItemGroup>
<Image Include="icon.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<Xsd Include="extra\layout.xsd">
<Filter>extras</Filter>
</Xsd>
</ItemGroup>
</Project>

View File

@@ -2,7 +2,11 @@
# 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/PanoPainterAndroidLegacyCompat.cmake)
link_directories(
../../libs/curl-android-ios/android/${ANDROID_ABI}
@@ -23,11 +27,66 @@ add_library(yuv SHARED IMPORTED)
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../../libs/libyuv/lib/android/${ANDROID_ABI}/libyuv.so)
# now build app's shared lib
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
set(PP_MODERN_COMPONENT_SOURCES
../../src/app_core/document_export.cpp
../../src/app_core/document_route.cpp
../../src/app_core/document_session.cpp
../../src/assets/brush_package.cpp
../../src/assets/image_format.cpp
../../src/assets/image_metadata.cpp
../../src/assets/image_pixels.cpp
../../src/assets/ppi_header.cpp
../../src/assets/settings_document.cpp
../../src/document/document.cpp
../../src/document/ppi_export.cpp
../../src/document/ppi_import.cpp
../../src/foundation/binary_stream.cpp
../../src/foundation/event.cpp
../../src/foundation/log.cpp
../../src/foundation/parse.cpp
../../src/foundation/task_queue.cpp
../../src/foundation/trace.cpp
../../src/paint/blend.cpp
../../src/paint/brush.cpp
../../src/paint/stroke.cpp
../../src/paint/stroke_script.cpp
../../src/paint_renderer/compositor.cpp
../../src/platform_api/asset_file_load_policy.cpp
../../src/platform_api/network_tls_policy.cpp
../../src/platform_api/platform_policy.cpp
../../src/platform_api/platform_services.cpp
../../src/platform_legacy/legacy_platform_services.cpp
../../src/renderer_api/recording_renderer.cpp
../../src/renderer_api/renderer_api.cpp
../../src/renderer_api/shader_catalog.cpp
../../src/renderer_gl/command_plan.cpp
../../src/renderer_gl/opengl_capabilities.cpp
../../src/renderer_gl/shader_bindings.cpp
../../src/legacy_app_dialog_services.cpp
../../src/legacy_app_preference_services.cpp
../../src/legacy_app_shell_services.cpp
../../src/legacy_app_startup_services.cpp
../../src/legacy_brush_package_export_services.cpp
../../src/legacy_brush_package_import_services.cpp
../../src/legacy_brush_ui_services.cpp
../../src/legacy_canvas_tool_services.cpp
../../src/legacy_canvas_view_services.cpp
../../src/legacy_cloud_services.cpp
../../src/legacy_document_animation_services.cpp
../../src/legacy_document_canvas_services.cpp
../../src/legacy_document_export_services.cpp
../../src/legacy_document_layer_services.cpp
../../src/legacy_document_open_services.cpp
../../src/legacy_document_session_services.cpp
../../src/legacy_grid_ui_services.cpp
../../src/legacy_history_services.cpp
../../src/legacy_quick_ui_services.cpp
../../src/legacy_recording_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 +187,10 @@ add_library(
../../src/node_metadata.cpp
)
target_compile_features(native-lib PRIVATE cxx_std_23)
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
pp_configure_legacy_nanort_overlay(native-lib)
target_include_directories(native-lib PRIVATE
src/main/cpp
../src/cpp

View File

@@ -0,0 +1,19 @@
set(PP_ANDROID_LEGACY_COMPAT_DIR "${CMAKE_CURRENT_LIST_DIR}")
function(pp_configure_legacy_nanort_overlay target_name)
set(nanort_source "${PP_ANDROID_LEGACY_COMPAT_DIR}/../../libs/nanort/nanort.h")
set(nanort_overlay_dir "${CMAKE_CURRENT_BINARY_DIR}/generated/nanort_compat")
set(nanort_overlay_header "${nanort_overlay_dir}/nanort.h")
file(READ "${nanort_source}" nanort_header)
string(REPLACE
" const size_t vertex_stride_bytes_;"
" size_t vertex_stride_bytes_;"
nanort_header
"${nanort_header}")
file(MAKE_DIRECTORY "${nanort_overlay_dir}")
file(WRITE "${nanort_overlay_header}" "${nanort_header}")
target_include_directories(${target_name} BEFORE PRIVATE "${nanort_overlay_dir}")
endfunction()

View File

@@ -2,7 +2,11 @@
# 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/PanoPainterAndroidLegacyCompat.cmake)
# build native_app_glue as a static lib
add_library(
@@ -17,23 +21,79 @@ set_target_properties(
${CMAKE_SOURCE_DIR}/../../libs/wave_sdk/wvr_client/lib/${ANDROID_ABI}/libwvr_api.so
)
set(PP_MODERN_COMPONENT_SOURCES
../../src/app_core/document_export.cpp
../../src/app_core/document_route.cpp
../../src/app_core/document_session.cpp
../../src/assets/brush_package.cpp
../../src/assets/image_format.cpp
../../src/assets/image_metadata.cpp
../../src/assets/image_pixels.cpp
../../src/assets/ppi_header.cpp
../../src/assets/settings_document.cpp
../../src/document/document.cpp
../../src/document/ppi_export.cpp
../../src/document/ppi_import.cpp
../../src/foundation/binary_stream.cpp
../../src/foundation/event.cpp
../../src/foundation/log.cpp
../../src/foundation/parse.cpp
../../src/foundation/task_queue.cpp
../../src/foundation/trace.cpp
../../src/paint/blend.cpp
../../src/paint/brush.cpp
../../src/paint/stroke.cpp
../../src/paint/stroke_script.cpp
../../src/paint_renderer/compositor.cpp
../../src/platform_api/asset_file_load_policy.cpp
../../src/platform_api/network_tls_policy.cpp
../../src/platform_api/platform_policy.cpp
../../src/platform_api/platform_services.cpp
../../src/platform_legacy/legacy_platform_services.cpp
../../src/renderer_api/recording_renderer.cpp
../../src/renderer_api/renderer_api.cpp
../../src/renderer_api/shader_catalog.cpp
../../src/renderer_gl/command_plan.cpp
../../src/renderer_gl/opengl_capabilities.cpp
../../src/renderer_gl/shader_bindings.cpp
../../src/legacy_app_dialog_services.cpp
../../src/legacy_app_preference_services.cpp
../../src/legacy_app_shell_services.cpp
../../src/legacy_app_startup_services.cpp
../../src/legacy_brush_package_export_services.cpp
../../src/legacy_brush_package_import_services.cpp
../../src/legacy_brush_ui_services.cpp
../../src/legacy_canvas_tool_services.cpp
../../src/legacy_canvas_view_services.cpp
../../src/legacy_cloud_services.cpp
../../src/legacy_document_animation_services.cpp
../../src/legacy_document_canvas_services.cpp
../../src/legacy_document_export_services.cpp
../../src/legacy_document_layer_services.cpp
../../src/legacy_document_open_services.cpp
../../src/legacy_document_session_services.cpp
../../src/legacy_grid_ui_services.cpp
../../src/legacy_history_services.cpp
../../src/legacy_quick_ui_services.cpp
../../src/legacy_recording_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 +181,10 @@ add_library(
../../src/settings.cpp
)
target_compile_features(native-lib PRIVATE cxx_std_23)
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
pp_configure_legacy_nanort_overlay(native-lib)
target_include_directories(native-lib PRIVATE
../../libs/wave_sdk/wvr_client/include
src/main/cpp

View File

@@ -2,7 +2,11 @@
# 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/PanoPainterAndroidLegacyCompat.cmake)
# build native_app_glue as a static lib
add_library(
@@ -25,23 +29,79 @@ set_target_properties(
)
set(PP_MODERN_COMPONENT_SOURCES
../../src/app_core/document_export.cpp
../../src/app_core/document_route.cpp
../../src/app_core/document_session.cpp
../../src/assets/brush_package.cpp
../../src/assets/image_format.cpp
../../src/assets/image_metadata.cpp
../../src/assets/image_pixels.cpp
../../src/assets/ppi_header.cpp
../../src/assets/settings_document.cpp
../../src/document/document.cpp
../../src/document/ppi_export.cpp
../../src/document/ppi_import.cpp
../../src/foundation/binary_stream.cpp
../../src/foundation/event.cpp
../../src/foundation/log.cpp
../../src/foundation/parse.cpp
../../src/foundation/task_queue.cpp
../../src/foundation/trace.cpp
../../src/paint/blend.cpp
../../src/paint/brush.cpp
../../src/paint/stroke.cpp
../../src/paint/stroke_script.cpp
../../src/paint_renderer/compositor.cpp
../../src/platform_api/asset_file_load_policy.cpp
../../src/platform_api/network_tls_policy.cpp
../../src/platform_api/platform_policy.cpp
../../src/platform_api/platform_services.cpp
../../src/platform_legacy/legacy_platform_services.cpp
../../src/renderer_api/recording_renderer.cpp
../../src/renderer_api/renderer_api.cpp
../../src/renderer_api/shader_catalog.cpp
../../src/renderer_gl/command_plan.cpp
../../src/renderer_gl/opengl_capabilities.cpp
../../src/renderer_gl/shader_bindings.cpp
../../src/legacy_app_dialog_services.cpp
../../src/legacy_app_preference_services.cpp
../../src/legacy_app_shell_services.cpp
../../src/legacy_app_startup_services.cpp
../../src/legacy_brush_package_export_services.cpp
../../src/legacy_brush_package_import_services.cpp
../../src/legacy_brush_ui_services.cpp
../../src/legacy_canvas_tool_services.cpp
../../src/legacy_canvas_view_services.cpp
../../src/legacy_cloud_services.cpp
../../src/legacy_document_animation_services.cpp
../../src/legacy_document_canvas_services.cpp
../../src/legacy_document_export_services.cpp
../../src/legacy_document_layer_services.cpp
../../src/legacy_document_open_services.cpp
../../src/legacy_document_session_services.cpp
../../src/legacy_grid_ui_services.cpp
../../src/legacy_history_services.cpp
../../src/legacy_quick_ui_services.cpp
../../src/legacy_recording_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 +189,10 @@ add_library(
../../src/settings.cpp
)
target_compile_features(native-lib PRIVATE cxx_std_23)
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
pp_configure_legacy_nanort_overlay(native-lib)
target_include_directories(native-lib PRIVATE
../../libs/ovr_mobile/include
../../libs/ovr_platform/Include

View File

@@ -35,7 +35,9 @@
#include "keymap.h"
#include "main.h"
#include "settings.h"
#if __has_include("com_omixlab_panopainter_MainActivity.h")
#include "com_omixlab_panopainter_MainActivity.h"
#endif
#ifdef __QUEST__
#include "oculus_vr.h"
@@ -241,7 +243,7 @@ extern "C"
#ifdef __FOCUS__
JNIEXPORT void JNICALL Java_com_omixlab_panopainter_MainActivity_init_vr(JNIEnv *env, jobject activity, jobject am)
{
Asset::m_am = AAssetManager_fromJava(env, am);
Asset::set_android_asset_manager(AAssetManager_fromJava(env, am));
wave_init(0, 0, 0);
}
#endif
@@ -700,7 +702,7 @@ static int engine_init_display(struct engine* engine) {
LOG("PROP Maker: %s", os_props["ro.product.manufacturer"].c_str());
LOG("PROP Mode: %s", os_props["ro.product.model"].c_str());
Asset::m_am = engine->app->activity->assetManager;
Asset::set_android_asset_manager(engine->app->activity->assetManager);
App::I->and_app = engine->app;
App::I->and_engine = engine;

View 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()

View File

@@ -0,0 +1,20 @@
option(PP_BUILD_APP "Build the PanoPainter application target from root CMake." ON)
option(PP_BUILD_TESTS "Build PanoPainter tests." ON)
option(PP_BUILD_TOOLS "Build PanoPainter automation tools." ON)
option(PP_ENABLE_OPENGL "Enable the OpenGL renderer backend." ON)
option(PP_ENABLE_VULKAN_EXPERIMENTAL "Enable non-production Vulkan experiments." OFF)
option(PP_ENABLE_VR "Enable VR support." ON)
option(PP_ENABLE_CLOUD "Enable cloud/network features." ON)
option(PP_ENABLE_VIDEO "Enable MP4/timelapse video features." ON)
option(PP_ENABLE_ASAN "Enable AddressSanitizer where supported." OFF)
option(PP_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer where supported." OFF)
option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where supported." OFF)
option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF)
option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF)
option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF)
option(PP_USE_VCPKG_TINYXML2 "Use the vcpkg tinyxml2 package for component targets." OFF)
set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.")
set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus)

View File

@@ -0,0 +1,44 @@
include(PanoPainterAutomation)
pp_add_powershell_automation_target(panopainter_package_readiness
COMMENT "Report package readiness blockers."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
-ReadinessOnly)
pp_add_powershell_automation_target(panopainter_windows_app_package_smoke
COMMENT "Build the Windows app artifact and report package readiness blockers."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
-Preset windows-msvc-default
-Configuration Debug
-Target PanoPainter
-CMakeCommand "${CMAKE_COMMAND}")
pp_add_powershell_automation_target(panopainter_android_standard_native_package
COMMENT "Build retained Android standard native package library."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
-Packages standard)
pp_add_powershell_automation_target(panopainter_android_vr_native_package_configure
COMMENT "Configure retained Android Quest/Focus native package paths."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
-Packages quest,focus
-ConfigureOnly)
pp_add_powershell_automation_target(panopainter_android_native_package_smoke
COMMENT "Run retained Android native package checks through package-smoke."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
-ReadinessOnly
-AndroidNativeChecks
-PackageKinds android-standard-apk,android-quest-apk,android-focus-apk)
pp_add_powershell_automation_target(panopainter_linux_webgl_package_readiness
COMMENT "Report retained Linux and WebGL package readiness blockers."
ARGUMENTS
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
-ReadinessOnly
-PackageKinds linux-app,webgl)

View 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)

View File

@@ -0,0 +1,29 @@
function(pp_configure_windows_runtime_payloads target_name)
if(NOT TARGET "${target_name}")
message(FATAL_ERROR "pp_configure_windows_runtime_payloads target does not exist: ${target_name}")
endif()
add_custom_command(TARGET "${target_name}" POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data"
"$<TARGET_FILE_DIR:${target_name}>/data"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
"$<TARGET_FILE_DIR:${target_name}>/BugTrapU-x64.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
"$<TARGET_FILE_DIR:${target_name}>/"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
"$<TARGET_FILE_DIR:${target_name}>/libyuv.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
"$<TARGET_FILE_DIR:${target_name}>/libmp4v2.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
"$<TARGET_FILE_DIR:${target_name}>/openh264-2.0.0-win64.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/bin/win64/openvr_api.dll"
"$<TARGET_FILE_DIR:${target_name}>/openvr_api.dll"
VERBATIM)
endfunction()

View File

@@ -0,0 +1,212 @@
set(PP_LEGACY_ENGINE_SOURCES
src/hmd.cpp
src/log.cpp
src/mp4enc.cpp
src/util.cpp
src/wacom.cpp
)
set(PP_LEGACY_ASSETS_IO_SOURCES
src/abr.cpp
src/asset.cpp
src/binary_stream.cpp
src/image.cpp
src/serializer.cpp
src/settings.cpp
)
set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
src/action.cpp
src/bezier.cpp
src/brush.cpp
src/canvas.cpp
src/canvas_actions.cpp
src/canvas_layer.cpp
src/event.cpp
)
set(PP_LEGACY_RENDERER_GL_SOURCES
src/font.cpp
src/rtt.cpp
src/shader.cpp
src/shape.cpp
src/texture.cpp
)
set(PP_LEGACY_UI_CORE_SOURCES
src/layout.cpp
src/node.cpp
src/node_border.cpp
src/node_button.cpp
src/node_button_custom.cpp
src/node_checkbox.cpp
src/node_combobox.cpp
src/node_icon.cpp
src/node_image.cpp
src/node_image_texture.cpp
src/node_input_box.cpp
src/node_popup_menu.cpp
src/node_progress_bar.cpp
src/node_remote_page.cpp
src/node_scroll.cpp
src/node_settings.cpp
src/node_shorcuts.cpp
src/node_slider.cpp
src/node_text.cpp
src/node_text_input.cpp
)
set(PP_LEGACY_APP_SOURCES
src/canvas_modes.cpp
src/legacy_app_shell_services.cpp
src/legacy_app_shell_services.h
src/legacy_canvas_tool_services.cpp
src/legacy_canvas_tool_services.h
src/legacy_canvas_view_services.cpp
src/legacy_canvas_view_services.h
src/legacy_document_canvas_services.cpp
src/legacy_document_canvas_services.h
src/legacy_document_layer_services.cpp
src/legacy_document_layer_services.h
src/legacy_history_services.cpp
src/legacy_history_services.h
src/legacy_recording_services.cpp
src/legacy_recording_services.h
src/legacy_ui_overlay_services.cpp
src/legacy_ui_overlay_services.h
src/pch.cpp
)
set(PP_PANOPAINTER_APP_SOURCES
src/app.cpp
src/app_cloud.cpp
src/app_commands.cpp
src/app_dialogs.cpp
src/app_events.cpp
src/app_layout.cpp
src/app_shaders.cpp
src/app_vr.cpp
src/legacy_app_dialog_services.cpp
src/legacy_app_dialog_services.h
src/legacy_app_preference_services.cpp
src/legacy_app_preference_services.h
src/legacy_app_startup_services.cpp
src/legacy_app_startup_services.h
src/legacy_brush_package_import_services.cpp
src/legacy_brush_package_import_services.h
src/legacy_brush_package_export_services.cpp
src/legacy_brush_package_export_services.h
src/legacy_cloud_services.cpp
src/legacy_cloud_services.h
src/legacy_document_export_services.cpp
src/legacy_document_export_services.h
src/legacy_document_open_services.cpp
src/legacy_document_open_services.h
src/legacy_document_session_services.cpp
src/legacy_document_session_services.h
src/platform_legacy/legacy_platform_services.cpp
src/platform_legacy/legacy_platform_services.h
src/version.cpp
)
set(PP_PANOPAINTER_UI_SOURCES
src/legacy_brush_ui_services.cpp
src/legacy_brush_ui_services.h
src/legacy_document_animation_services.cpp
src/legacy_document_animation_services.h
src/legacy_grid_ui_services.cpp
src/legacy_grid_ui_services.h
src/legacy_quick_ui_services.cpp
src/legacy_quick_ui_services.h
src/node_about.cpp
src/node_canvas.cpp
src/node_changelog.cpp
src/node_color_quad.cpp
src/node_colorwheel.cpp
src/node_dialog_browse.cpp
src/node_dialog_cloud.cpp
src/node_dialog_export_ppbr.cpp
src/node_dialog_layer_rename.cpp
src/node_dialog_open.cpp
src/node_dialog_picker.cpp
src/node_dialog_resize.cpp
src/node_message_box.cpp
src/node_metadata.cpp
src/node_panel_animation.cpp
src/node_panel_brush.cpp
src/node_panel_color.cpp
src/node_panel_floating.cpp
src/node_panel_grid.cpp
src/node_panel_layer.cpp
src/node_panel_quick.cpp
src/node_panel_stroke.cpp
src/node_stroke_preview.cpp
src/node_tool_bucket.cpp
src/node_usermanual.cpp
src/node_viewport.cpp
)
set(PP_WINDOWS_PLATFORM_SOURCES
src/main.cpp
src/platform_windows/windows_platform_services.cpp
src/platform_windows/windows_platform_services.h
)
set(PP_WINDOWS_APP_SOURCES
PanoPainter.rc
)
set(PP_VENDOR_SOURCES
libs/fmt/src/format.cc
libs/fmt/src/posix.cc
libs/glad/src/glad.c
libs/glad/src/glad_wgl.c
libs/hash-library/md5.cpp
libs/jpeg/jpgd.cpp
libs/jpeg/jpge.cpp
libs/nanort/nanort.cc
libs/poly2tri/poly2tri/common/shapes.cc
libs/poly2tri/poly2tri/sweep/advancing_front.cc
libs/poly2tri/poly2tri/sweep/cdt.cc
libs/poly2tri/poly2tri/sweep/sweep.cc
libs/poly2tri/poly2tri/sweep/sweep_context.cc
libs/sqlite3/sqlite3.c
libs/tinyxml2/tinyxml2.cpp
libs/wacom/WinTab/Utils.cpp
libs/yoga/yoga/event/event.cpp
libs/yoga/yoga/internal/experiments.cpp
libs/yoga/yoga/log.cpp
libs/yoga/yoga/Utils.cpp
libs/yoga/yoga/YGConfig.cpp
libs/yoga/yoga/YGEnums.cpp
libs/yoga/yoga/YGLayout.cpp
libs/yoga/yoga/YGNode.cpp
libs/yoga/yoga/YGNodePrint.cpp
libs/yoga/yoga/YGStyle.cpp
libs/yoga/yoga/YGValue.cpp
libs/yoga/yoga/Yoga.cpp
)
set(PP_LEGACY_INCLUDE_DIRS
"${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/base64"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/fmt/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glad/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/glm"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/hash-library"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/jpeg"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/nanort"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/include"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/headers"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/poly2tri/poly2tri"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/sqlite3"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
)

View File

@@ -0,0 +1,17 @@
function(pp_add_version_generation target config_name)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
add_custom_command(
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h"
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py" "${config_name}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pre-build.py"
COMMENT "Generating src/version.gen.h"
VERBATIM)
add_custom_target(pp_generate_version
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/version.gen.h")
add_dependencies(${target} pp_generate_version)
endfunction()

View File

@@ -0,0 +1,44 @@
function(pp_configure_project_warnings target)
if(MSVC)
target_compile_options(${target} INTERFACE
/W4
/permissive-
/Zc:__cplusplus
/Zc:preprocessor
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
/wd4100)
if(PP_ENABLE_MSVC_ANALYZE)
target_compile_options(${target} INTERFACE /analyze)
endif()
else()
target_compile_options(${target} INTERFACE
-Wall
-Wextra
-Wpedantic
-Wconversion
-Wshadow
-Wnull-dereference
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
-Wno-unused-parameter)
endif()
if(PP_ENABLE_ASAN)
if(MSVC)
target_compile_options(${target} INTERFACE /fsanitize=address)
target_link_options(${target} INTERFACE /fsanitize=address)
else()
target_compile_options(${target} INTERFACE -fsanitize=address)
target_link_options(${target} INTERFACE -fsanitize=address)
endif()
endif()
if(PP_ENABLE_UBSAN AND NOT MSVC)
target_compile_options(${target} INTERFACE -fsanitize=undefined)
target_link_options(${target} INTERFACE -fsanitize=undefined)
endif()
if(PP_ENABLE_TSAN AND NOT MSVC)
target_compile_options(${target} INTERFACE -fsanitize=thread)
target_link_options(${target} INTERFACE -fsanitize=thread)
endif()
endfunction()

View File

@@ -0,0 +1,75 @@
if(NOT DEFINED PP_SHADER_DIR)
message(FATAL_ERROR "PP_SHADER_DIR is required")
endif()
file(REAL_PATH "${PP_SHADER_DIR}" pp_shader_dir)
if(NOT IS_DIRECTORY "${pp_shader_dir}")
message(FATAL_ERROR "Shader directory does not exist: ${pp_shader_dir}")
endif()
file(GLOB_RECURSE pp_shader_files
"${pp_shader_dir}/*.glsl")
if(NOT pp_shader_files)
message(FATAL_ERROR "No shader files found under: ${pp_shader_dir}")
endif()
set(pp_shader_errors "")
set(pp_top_level_count 0)
set(pp_include_count 0)
foreach(pp_shader_file IN LISTS pp_shader_files)
file(RELATIVE_PATH pp_shader_rel "${pp_shader_dir}" "${pp_shader_file}")
file(READ "${pp_shader_file}" pp_shader_contents)
string(REGEX MATCHALL "#[ \t]*include[ \t]+\"[^\"]+\"" pp_include_lines "${pp_shader_contents}")
foreach(pp_include_line IN LISTS pp_include_lines)
string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" pp_include_path "${pp_include_line}")
if(pp_include_path MATCHES "^/")
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must be relative: ${pp_include_path}")
endif()
if(pp_include_path MATCHES "^[A-Za-z]:")
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not be drive-absolute: ${pp_include_path}")
endif()
if(pp_include_path MATCHES "\\.\\.")
list(APPEND pp_shader_errors "${pp_shader_rel}: include path must not traverse parent directories: ${pp_include_path}")
endif()
if(NOT EXISTS "${pp_shader_dir}/${pp_include_path}")
list(APPEND pp_shader_errors "${pp_shader_rel}: missing include: ${pp_include_path}")
endif()
endforeach()
if(pp_shader_rel MATCHES "^include/")
math(EXPR pp_include_count "${pp_include_count} + 1")
if(pp_shader_contents MATCHES "\\[\\[(vertex|fragment)\\]\\]")
list(APPEND pp_shader_errors "${pp_shader_rel}: include shaders must not declare stage markers")
endif()
else()
math(EXPR pp_top_level_count "${pp_top_level_count} + 1")
string(REGEX MATCHALL "\\[\\[vertex\\]\\]" pp_vertex_markers "${pp_shader_contents}")
string(REGEX MATCHALL "\\[\\[fragment\\]\\]" pp_fragment_markers "${pp_shader_contents}")
list(LENGTH pp_vertex_markers pp_vertex_count)
list(LENGTH pp_fragment_markers pp_fragment_count)
if(NOT pp_vertex_count EQUAL 1)
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[vertex]] marker")
endif()
if(NOT pp_fragment_count EQUAL 1)
list(APPEND pp_shader_errors "${pp_shader_rel}: expected exactly one [[fragment]] marker")
endif()
string(FIND "${pp_shader_contents}" "[[vertex]]" pp_vertex_pos)
string(FIND "${pp_shader_contents}" "[[fragment]]" pp_fragment_pos)
if(pp_vertex_pos GREATER_EQUAL 0 AND pp_fragment_pos GREATER_EQUAL 0 AND NOT pp_vertex_pos LESS pp_fragment_pos)
list(APPEND pp_shader_errors "${pp_shader_rel}: [[vertex]] marker must appear before [[fragment]]")
endif()
endif()
endforeach()
if(pp_shader_errors)
list(JOIN pp_shader_errors "\n" pp_shader_error_text)
message(FATAL_ERROR "Shader validation failed:\n${pp_shader_error_text}")
endif()
message(STATUS "Validated ${pp_top_level_count} shader programs and ${pp_include_count} shader includes under ${pp_shader_dir}")

View File

@@ -0,0 +1,49 @@
# ADR 0001: Incremental Component Boundaries
Status: accepted
Date: 2026-05-31
## Context
PanoPainter currently has a flat `src/` layout with broad dependencies through
`pch.h`, global singletons such as `App::I` and `Canvas::I`, OpenGL types in
high-level painting/document headers, and duplicated platform source lists.
The modernization work must retain existing behavior across Windows desktop
and AppX, macOS, iOS, Android standard, Quest, Focus/Wave, Linux, and WebGL.
## Decision
Modernization will proceed incrementally. OpenGL remains the production
renderer while component boundaries and tests are introduced. Vulkan, Metal,
and WebGPU-related work must stay out of the production path until OpenGL
parity tests exist.
The target dependency direction is:
```text
pp_foundation
-> pp_assets
-> pp_paint
-> pp_document
-> pp_renderer_api
-> pp_renderer_gl
-> pp_paint_renderer
-> pp_ui_core
-> pp_panopainter_ui
-> pp_platform_*
-> panopainter_app
```
Pure component headers must not include platform SDK headers or graphics API
headers. Temporary shims are allowed only when recorded in
`docs/modernization/debt.md`.
## Consequences
- The first implementation steps are documentation, inventory, CMake skeleton,
diagnostics, and tests, not a renderer rewrite.
- Existing project files remain until the shared CMake targets are validated.
- Refactors should prefer additive compatibility layers before moving behavior.
- Every extracted component must gain its own tests before the next component
boundary is extracted.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
# PanoPainter Capability Map
Status: live
Last updated: 2026-06-05
This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label,
and validation command.
## Project And Documents
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture, live-canvas-to-`pp_document` snapshot projection with captured RGBA8 payloads, pending renderer-readback counts, save-readiness reporting before retained live saves, pure PPI export from payload-complete snapshots, and shared paint-renderer export-readiness reporting from the same snapshot |
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input, destination-feedback copy/fetch gate |
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
| App status and renderer diagnostics | App title/status widgets, extension indicators | `pp_app_core`, `pp_renderer_api`, `pp_panopainter_ui` | Title/status text tests, renderer diagnostic indicator tests, CLI status smoke, live layout adapter smoke |
## Image And Export
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness through the shared paint-renderer export report, pure cube-face PNG writer, pure equirectangular PNG/JPEG+XMP writers, pure layer/frame collection PNG writers, app-core collection write executor, retained fallback coverage |
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload/face-PNG readiness report, pure document-frame equirectangular PNG and JPEG+XMP export with live writer fallback, pure layer/frame equirectangular PNG collection export, exact GPU/golden parity |
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, shared export-readiness report, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
| Depth export | `Canvas`, grid tools | `pp_paint_renderer`, `pp_app_core` | Depth target/write planning, document-snapshot renderer-readiness logging, depth render-plan draw/readback counts, retained render/readback parity, and format/golden validation |
## Brush And Painting
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Brush settings serialization and stroke-panel controls | `Brush`, `Serializer`, `NodePanelStroke` | `pp_paint`, `pp_assets`, `pp_app_core`, `pp_panopainter_ui` | Round-trip and boundary values; stroke slider/toggle/blend/reset planning and invalid setting tests |
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden |
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, pure `pp_document` face and six-face frame compositing plus renderer texture upload/OpenGL command-plan coverage, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke/thumbnail/brush-preview destination-copy coverage, and GPU parity |
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Layer rename/add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document`, `pp_app_core` | Rename and operation planning, service-dispatch, no-op preservation, undo/redo invariant tests |
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_app_core`, `pp_paint_renderer` | Metadata planning, service-dispatch, live-canvas-to-`pp_document` snapshot projection, CPU model and render golden |
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
## UI And Workflow
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML |
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests |
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained overlay-adapter build coverage, retained popup/dialog lifetime tests |
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
## Input, Platform, And Devices
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback |
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |
| Desktop XR | `HMD`, `Vive`, `app_vr`, retained OpenVR bridge | `pp_platform_vr`, app with OpenXR backend | Runtime-selection policy tests, compile gate, and mocked pose tests |
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
## Cloud, Logging, And Automation
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Upload/download/browse | `app_cloud`, CURL helpers | `pp_app_core`, app service, `pp_platform_*` | Upload prompt/new-doc/no-canvas decision tests, bulk-upload progress decision tests, browse/selection decision tests, mocked HTTP and timeout tests |
| License/check flows | app/cloud code | app service | Mocked response tests |
| Logging/crash reporting | `log`, BugTrap/AppCenter | `pp_foundation`, platform wrappers | Log formatting and platform compile |
| Headless automation | none yet | `tools/pano_cli` | JSON command fixtures |
| Tracing | none yet | `pp_foundation` | Span nesting/timing tests |

761
docs/modernization/debt.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,7 @@ BOOL LoadWintab( void )
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wacom_Tablet.dll" );
// ghWintab = LoadLibraryA( "C:\\dev\\mainline\\Wacom\\Win\\Win32\\Debug\\Wintab32.dll" );
LOG("calling LoadLibrary");
ghWintab = LoadLibrary(L"Wintab32.dll");
ghWintab = LoadLibraryW(L"Wintab32.dll");
LOG("LoadLibrary called");
if ( !ghWintab )

View File

@@ -1,7 +1,5 @@
cmake_minimum_required(VERSION 3.4.1)
project(panopainter)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
cmake_minimum_required(VERSION 3.10)
project(PanoPainterLinux LANGUAGES C CXX)
add_executable(panopainter
src/main.cpp
@@ -120,4 +118,7 @@ target_include_directories(panopainter PRIVATE
)
target_link_libraries(panopainter glfw curl GL dl X11 pthread)
target_compile_features(panopainter PRIVATE cxx_std_23)
set_target_properties(panopainter PROPERTIES
CXX_EXTENSIONS OFF)
target_compile_definitions(panopainter PUBLIC "$<$<CONFIG:DEBUG>:_DEBUG>")

View File

@@ -0,0 +1,65 @@
[CmdletBinding()]
param(
[string]$Preset = "windows-msvc-default",
[switch]$NoApp
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$argsList = @(
"--preset", $Preset,
"-DPP_ENABLE_MSVC_ANALYZE=ON",
"-DPP_ENABLE_CLANG_TIDY=ON",
"-DPP_ENABLE_CPPCHECK=ON"
)
if ($NoApp) {
$argsList += "-DPP_BUILD_APP=OFF"
}
& cmake @argsList
$configureExitCode = $LASTEXITCODE
$shaderExitCode = 0
$rendererBoundaryExitCode = 0
if ($configureExitCode -eq 0) {
& cmake --build --preset $Preset --target panopainter_validate_shaders
$shaderExitCode = $LASTEXITCODE
}
if ($configureExitCode -eq 0) {
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check-renderer-boundary.ps1")
$rendererBoundaryExitCode = $LASTEXITCODE
}
$exitCode = $configureExitCode
if ($exitCode -eq 0 -and $shaderExitCode -ne 0) {
$exitCode = $shaderExitCode
}
if ($exitCode -eq 0 -and $rendererBoundaryExitCode -ne 0) {
$exitCode = $rendererBoundaryExitCode
}
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "analyze"
preset = $Preset
exitCode = $exitCode
checks = @(
[ordered]@{
name = "configure"
exitCode = $configureExitCode
},
[ordered]@{
name = "shader-validation"
exitCode = $shaderExitCode
},
[ordered]@{
name = "renderer-boundary"
exitCode = $rendererBoundaryExitCode
}
)
elapsedMs = $elapsed
} | ConvertTo-Json -Compress -Depth 4
exit $exitCode

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env sh
set -u
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
preset="${1:-linux-clang}"
start="$(date +%s)"
cmake --preset "$preset" -DPP_ENABLE_CLANG_TIDY=ON -DPP_ENABLE_CPPCHECK=ON
configure_exit_code="$?"
shader_exit_code="0"
renderer_boundary_exit_code="0"
if [ "$configure_exit_code" -eq 0 ]; then
cmake --build --preset "$preset" --target panopainter_validate_shaders
shader_exit_code="$?"
fi
if [ "$configure_exit_code" -eq 0 ]; then
"$script_dir/check-renderer-boundary.sh"
renderer_boundary_exit_code="$?"
fi
exit_code="$configure_exit_code"
if [ "$exit_code" -eq 0 ] && [ "$shader_exit_code" -ne 0 ]; then
exit_code="$shader_exit_code"
fi
if [ "$exit_code" -eq 0 ] && [ "$renderer_boundary_exit_code" -ne 0 ]; then
exit_code="$renderer_boundary_exit_code"
fi
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s},{"name":"renderer-boundary","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$renderer_boundary_exit_code" "$elapsed_ms"
exit "$exit_code"

View 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

View 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
}
}

View 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"
}

View File

@@ -0,0 +1,93 @@
[CmdletBinding()]
param(
[string]$HostName = "panopainter-mac",
[string]$RemoteDirectory = "~/Dev/panopainter",
[string]$RepositoryUrl = "ssh://git@git.omar.synology.me:3022/omar/panopainter.git",
[string]$Branch = "codex/modernization-cmake-foundation",
[string[]]$Presets = @("macos", "ios-simulator", "ios-device")
)
$ErrorActionPreference = "Stop"
function Join-RemoteArgument {
param([string[]]$Values)
$expanded = @()
foreach ($value in $Values) {
foreach ($part in ($value -split ",")) {
$trimmed = $part.Trim()
if ($trimmed.Length -gt 0) {
$expanded += $trimmed
}
}
}
return ($expanded -join " ")
}
function ConvertTo-ShellSingleQuoted {
param([string]$Value)
return "'" + ($Value -replace "'", "'\\''") + "'"
}
$presetArgument = Join-RemoteArgument -Values $Presets
$remoteDirectoryLiteral = ConvertTo-ShellSingleQuoted -Value $RemoteDirectory
$repositoryLiteral = ConvertTo-ShellSingleQuoted -Value $RepositoryUrl
$branchLiteral = ConvertTo-ShellSingleQuoted -Value $Branch
$presetLiteral = ConvertTo-ShellSingleQuoted -Value $presetArgument
$remoteScript = @"
set -eu
export PATH="/opt/homebrew/bin:/usr/local/bin:`$HOME/tools/bin:`$PATH"
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
remote_dir=$remoteDirectoryLiteral
repository_url=$repositoryLiteral
branch_name=$branchLiteral
presets=$presetLiteral
case "`$remote_dir" in
"~/"*) remote_dir="`$HOME/`$(printf '%s' "`$remote_dir" | sed 's|^~/||')" ;;
esac
mkdir -p "`$(dirname "`$remote_dir")"
if [ ! -d "`$remote_dir/.git" ]; then
git clone "`$repository_url" "`$remote_dir"
fi
cd "`$remote_dir"
git fetch origin
git checkout "`$branch_name"
git pull --ff-only origin "`$branch_name"
git submodule update --init --recursive \
libs/tinyxml2 \
libs/glm \
libs/stb/stb \
libs/yoga \
libs/poly2tri \
libs/base64 \
libs/sqlite3 \
libs/nanort \
libs/hash-library \
libs/fmt \
libs/glad \
libs/tinyfiledialogs
mkdir -p out/logs
log="out/logs/apple-platform-build-`$(date +%Y%m%d-%H%M%S).log"
set +e
sh ./scripts/automation/platform-build.sh "`$presets" > "`$log" 2>&1
exit_code=`$?
set -e
printf '{"command":"apple-remote-build","host":"%s","branch":"%s","presets":"%s","log":"%s","exitCode":%s}\n' \
"`$(hostname)" "`$branch_name" "`$presets" "`$log" "`$exit_code"
tail -n 80 "`$log"
exit "`$exit_code"
"@
$remoteScript = $remoteScript -replace "`r`n", "`n"
$encodedRemoteScript = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($remoteScript))
& ssh -o BatchMode=yes $HostName "printf '%s' '$encodedRemoteScript' | base64 -D | sh"
exit $LASTEXITCODE

View File

@@ -0,0 +1,28 @@
[CmdletBinding()]
param(
[string]$Preset = "windows-msvc-default",
[string]$Configuration = "Debug",
[string]$Target = ""
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$argsList = @("--build", "--preset", $Preset, "--config", $Configuration)
if ($Target.Length -gt 0) {
$argsList += @("--target", $Target)
}
& cmake @argsList
$exitCode = $LASTEXITCODE
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "build"
preset = $Preset
configuration = $Configuration
target = $Target
exitCode = $exitCode
elapsedMs = $elapsed
} | ConvertTo-Json -Compress
exit $exitCode

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
set -u
preset="${1:-linux-clang}"
configuration="${2:-Debug}"
target="${3:-}"
start="$(date +%s)"
if [ -n "$target" ]; then
cmake --build --preset "$preset" --config "$configuration" --target "$target"
else
cmake --build --preset "$preset" --config "$configuration"
fi
exit_code="$?"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"build","preset":"%s","configuration":"%s","target":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$exit_code" "$elapsed_ms"
exit "$exit_code"

View File

@@ -0,0 +1,62 @@
[CmdletBinding()]
param(
[string]$Root = ""
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($Root)) {
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
}
$started = Get-Date
$pattern = '\b(?:GL|WGL)_[A-Z0-9_]+\b'
$allowed = @(
"src/renderer_gl/",
"src/rtt.cpp",
"src/texture.cpp"
)
$violations = @()
$files = Get-ChildItem -Path (Join-Path $Root "src") -Recurse -File -Include *.c,*.cc,*.cpp,*.h,*.hpp
foreach ($file in $files) {
$relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/').Replace('\', '/')
$isAllowed = $false
foreach ($prefix in $allowed) {
if ($relative.StartsWith($prefix)) {
$isAllowed = $true
break
}
}
if ($isAllowed) {
continue
}
$lineNumber = 0
foreach ($line in Get-Content -LiteralPath $file.FullName) {
$lineNumber += 1
$trimmed = $line.TrimStart()
if ($trimmed.StartsWith("//")) {
continue
}
$matches = [regex]::Matches($line, $pattern)
foreach ($match in $matches) {
$violations += [ordered]@{
file = $relative
line = $lineNumber
token = $match.Value
}
}
}
}
$exitCode = if ($violations.Count -eq 0) { 0 } else { 1 }
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "check-renderer-boundary"
exitCode = $exitCode
violationCount = $violations.Count
violations = @($violations | Select-Object -First 50)
elapsedMs = $elapsed
} | ConvertTo-Json -Compress -Depth 4
exit $exitCode

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env sh
set -u
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
root="${1:-$(CDPATH= cd -- "$script_dir/../.." && pwd)}"
start="$(date +%s)"
tmp="${TMPDIR:-/tmp}/panopainter-renderer-boundary-$$.txt"
find "$root/src" -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) | while IFS= read -r file; do
rel="${file#"$root"/}"
case "$rel" in
src/renderer_gl/*|src/rtt.cpp|src/texture.cpp)
continue
;;
esac
awk -v rel="$rel" '
/^[[:space:]]*\/\// { next }
match($0, /\<(GL|WGL)_[A-Z0-9_]+\>/) {
print rel ":" FNR ":" substr($0, RSTART, RLENGTH)
}
' "$file"
done > "$tmp"
count="$(wc -l < "$tmp" | tr -d '[:space:]')"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
exit_code="0"
if [ "$count" -ne 0 ]; then
exit_code="1"
fi
printf '{"command":"check-renderer-boundary","exitCode":%s,"violationCount":%s,"elapsedMs":%s}\n' "$exit_code" "$count" "$elapsed_ms"
rm -f "$tmp"
exit "$exit_code"

View File

@@ -0,0 +1,25 @@
[CmdletBinding()]
param(
[string]$Preset = "windows-msvc-default",
[switch]$NoApp
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$argsList = @("--preset", $Preset)
if ($NoApp) {
$argsList += "-DPP_BUILD_APP=OFF"
}
& cmake @argsList
$exitCode = $LASTEXITCODE
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "configure"
preset = $Preset
exitCode = $exitCode
elapsedMs = $elapsed
} | ConvertTo-Json -Compress
exit $exitCode

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env sh
set -u
preset="${1:-linux-clang}"
start="$(date +%s)"
cmake --preset "$preset"
exit_code="$?"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"configure","preset":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$exit_code" "$elapsed_ms"
exit "$exit_code"

View File

@@ -0,0 +1,413 @@
[CmdletBinding()]
param(
[string]$Preset = "windows-msvc-default",
[string]$Configuration = "Debug",
[string]$Target = "PanoPainter",
[string]$CMakeCommand = "cmake",
[switch]$ReadinessOnly,
[switch]$AndroidNativeChecks,
[string[]]$PackageKinds = @(
"windows-appx",
"android-standard-apk",
"android-quest-apk",
"android-focus-apk",
"apple-bundle",
"linux-app",
"webgl"
)
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$root = (Get-Location).Path
function Test-CommandAvailable {
param([string]$Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
function Expand-ArgumentList {
param([string[]]$Values)
$expanded = @()
foreach ($value in $Values) {
foreach ($part in ($value -split ",")) {
$trimmed = $part.Trim()
if ($trimmed.Length -gt 0) {
$expanded += $trimmed
}
}
}
return $expanded
}
function New-ArtifactCheck {
param(
[string]$Name,
[string]$Path,
[string]$PathType = "Any"
)
$exists = if ($PathType -eq "Container") {
Test-Path -LiteralPath $Path -PathType Container
} elseif ($PathType -eq "Leaf") {
Test-Path -LiteralPath $Path -PathType Leaf
} else {
Test-Path -LiteralPath $Path
}
[ordered]@{
name = $Name
path = $Path
pathType = $PathType
exists = $exists
}
}
function New-Prerequisite {
param(
[string]$Name,
[bool]$Available,
[string]$Detail
)
[ordered]@{
name = $Name
available = $Available
detail = $Detail
}
}
function New-PackageReadiness {
param(
[string]$Kind,
[string]$Status,
[string]$Reason,
[object[]]$Prerequisites,
[object[]]$Artifacts,
[string]$ValidationCommand
)
[ordered]@{
kind = $Kind
status = $Status
reason = $Reason
debt = "DEBT-0011"
validationCommand = $ValidationCommand
prerequisites = $Prerequisites
artifacts = $Artifacts
}
}
function Get-AndroidNativeCheckPlan {
param([string[]]$Kinds)
$packages = @()
if ($Kinds -contains "android-standard-apk") {
$packages += [ordered]@{
packages = @("standard")
configureOnly = $false
}
}
$configureOnlyPackages = @()
if ($Kinds -contains "android-quest-apk") {
$configureOnlyPackages += "quest"
}
if ($Kinds -contains "android-focus-apk") {
$configureOnlyPackages += "focus"
}
if ($configureOnlyPackages.Count -gt 0) {
$packages += [ordered]@{
packages = $configureOnlyPackages
configureOnly = $true
}
}
return $packages
}
function Invoke-AndroidNativePackageChecks {
param([string[]]$Kinds)
$plans = @(Get-AndroidNativeCheckPlan -Kinds $Kinds)
$results = @()
$overallExitCode = 0
foreach ($plan in $plans) {
$arguments = @(
"-ExecutionPolicy", "Bypass",
"-File", (Join-Path $root "scripts/automation/android-legacy-package-build.ps1"),
"-Packages", ($plan.packages -join ",")
)
if ($plan.configureOnly) {
$arguments += "-ConfigureOnly"
}
$output = @(& powershell @arguments 2>&1)
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0 -and $overallExitCode -eq 0) {
$overallExitCode = $exitCode
}
$jsonLine = @($output | ForEach-Object { $_.ToString() } | Where-Object { $_.TrimStart().StartsWith("{") } | Select-Object -Last 1)
$summary = $null
if ($jsonLine.Count -gt 0) {
try {
$summary = $jsonLine[-1] | ConvertFrom-Json
} catch {
$summary = $null
}
}
$results += [ordered]@{
packages = $plan.packages
configureOnly = [bool]$plan.configureOnly
exitCode = $exitCode
command = "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages $($plan.packages -join ',')$(if ($plan.configureOnly) { ' -ConfigureOnly' } else { '' })"
summary = $summary
}
}
[ordered]@{
requested = $plans.Count -gt 0
exitCode = $overallExitCode
results = $results
}
}
$PackageKinds = @(Expand-ArgumentList -Values $PackageKinds)
function Get-PackageReadiness {
param([string[]]$Kinds)
$readiness = @()
foreach ($kind in $Kinds) {
switch ($kind) {
"windows-appx" {
$wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj"
$manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest"
$appPackages = Join-Path $root "PanoPainterPackage/AppPackages"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
-Prerequisites @(
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
)
}
"android-standard-apk" {
$gradle = Join-Path $root "android/android/build.gradle"
$manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/android/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/android assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"android-quest-apk" {
$gradle = Join-Path $root "android/quest/build.gradle"
$manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/quest/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/quest assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"android-focus-apk" {
$gradle = Join-Path $root "android/focus/build.gradle"
$manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/focus/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/focus assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"apple-bundle" {
$xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj"
$bundleDir = Join-Path $root "out/package/apple"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
-Prerequisites @(
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
)
}
"linux-app" {
$linuxCmake = Join-Path $root "linux/CMakeLists.txt"
$linuxBinary = Join-Path $root "out/package/linux/panopainter"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "retained-linux-cmake-not-consuming-root-cmake-targets" `
-ValidationCommand "cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter" `
-Prerequisites @(
(New-Prerequisite -Name "retained-linux-cmake" -Available (Test-Path -LiteralPath $linuxCmake -PathType Leaf) -Detail $linuxCmake),
(New-Prerequisite -Name "cmake" -Available (Test-CommandAvailable "cmake") -Detail "Linux retained app CMake configure/build tool"),
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "linux-clang"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "linux-app-output" -Path $linuxBinary -PathType "Leaf")
)
}
"webgl" {
$webglCmake = Join-Path $root "webgl/CMakeLists.txt"
$webDir = Join-Path $root "out/package/webgl"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "retained-webgl-cmake-not-consuming-root-cmake-targets" `
-ValidationCommand "emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter" `
-Prerequisites @(
(New-Prerequisite -Name "retained-webgl-cmake" -Available (Test-Path -LiteralPath $webglCmake -PathType Leaf) -Detail $webglCmake),
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
)
}
}
}
return $readiness
}
$androidNativeValidation = if ($AndroidNativeChecks) {
Invoke-AndroidNativePackageChecks -Kinds $PackageKinds
} else {
[ordered]@{
requested = $false
exitCode = 0
results = @()
}
}
if ($ReadinessOnly) {
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "package-smoke"
preset = $Preset
configuration = $Configuration
target = $Target
stage = "readiness"
exitCode = 0
elapsedMs = $elapsed
androidNativeValidation = $androidNativeValidation
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
} | ConvertTo-Json -Compress -Depth 8
exit $androidNativeValidation.exitCode
}
& $CMakeCommand --build --preset $Preset --config $Configuration --target $Target
$buildExitCode = $LASTEXITCODE
if ($buildExitCode -ne 0) {
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "package-smoke"
preset = $Preset
configuration = $Configuration
target = $Target
stage = "build"
cmakeCommand = $CMakeCommand
exitCode = $buildExitCode
elapsedMs = $elapsed
androidNativeValidation = $androidNativeValidation
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
} | ConvertTo-Json -Compress -Depth 8
exit $buildExitCode
}
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
$targetDir = Split-Path -Parent $binaryDir
$dataDir = Join-Path $targetDir "data"
$curlDll = if ($Configuration -eq "Debug") { "libcurl_debug.dll" } else { "libcurl.dll" }
$checks = @(
[ordered]@{ name = "executable"; path = $binaryDir; exists = Test-Path -LiteralPath $binaryDir -PathType Leaf },
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container },
[ordered]@{ name = "BugTrapU-x64.dll"; path = (Join-Path $targetDir "BugTrapU-x64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "BugTrapU-x64.dll") -PathType Leaf },
[ordered]@{ name = $curlDll; path = (Join-Path $targetDir $curlDll); exists = Test-Path -LiteralPath (Join-Path $targetDir $curlDll) -PathType Leaf },
[ordered]@{ name = "libyuv.dll"; path = (Join-Path $targetDir "libyuv.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libyuv.dll") -PathType Leaf },
[ordered]@{ name = "libmp4v2.dll"; path = (Join-Path $targetDir "libmp4v2.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libmp4v2.dll") -PathType Leaf },
[ordered]@{ name = "openh264-2.0.0-win64.dll"; path = (Join-Path $targetDir "openh264-2.0.0-win64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openh264-2.0.0-win64.dll") -PathType Leaf },
[ordered]@{ name = "openvr_api.dll"; path = (Join-Path $targetDir "openvr_api.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openvr_api.dll") -PathType Leaf }
)
$failed = @($checks | Where-Object { -not $_.exists })
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
if ($androidNativeValidation.exitCode -ne 0 -and $exitCode -eq 0) {
$exitCode = $androidNativeValidation.exitCode
}
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "package-smoke"
preset = $Preset
configuration = $Configuration
target = $Target
cmakeCommand = $CMakeCommand
exitCode = $exitCode
elapsedMs = $elapsedMs
checks = $checks
androidNativeValidation = $androidNativeValidation
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
} | ConvertTo-Json -Compress -Depth 8
exit $exitCode

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env sh
set -u
preset="${1:-linux-clang}"
configuration="${2:-Debug}"
target="${3:-PanoPainter}"
artifact="${4:-out/build/$preset/$target}"
readiness_only=0
if [ "${1:-}" = "--readiness-only" ]; then
readiness_only=1
preset="${2:-linux-clang}"
configuration="${3:-Debug}"
target="${4:-PanoPainter}"
artifact="${5:-out/build/$preset/$target}"
fi
start="$(date +%s)"
root="$(pwd)"
json_string() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
}
json_bool() {
if [ "$1" = "1" ]; then
printf true
else
printf false
fi
}
command_available() {
command -v "$1" >/dev/null 2>&1
}
file_available() {
[ -f "$1" ]
}
dir_available() {
[ -d "$1" ]
}
package_readiness_json() {
windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj"
windows_manifest="$root/PanoPainterPackage/Package.appxmanifest"
windows_output="$root/PanoPainterPackage/AppPackages"
android_standard_gradle="$root/android/android/build.gradle"
android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml"
android_standard_output="$root/android/android/build/outputs/apk"
android_quest_gradle="$root/android/quest/build.gradle"
android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml"
android_quest_output="$root/android/quest/build/outputs/apk"
android_focus_gradle="$root/android/focus/build.gradle"
android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml"
android_focus_output="$root/android/focus/build/outputs/apk"
apple_project="$root/PanoPainter.xcodeproj/project.pbxproj"
apple_output="$root/out/package/apple"
linux_cmake="$root/linux/CMakeLists.txt"
linux_output="$root/out/package/linux/panopainter"
webgl_cmake="$root/webgl/CMakeLists.txt"
webgl_output="$root/out/package/webgl"
file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$linux_cmake"; linux_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available cmake; cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$linux_output"; linux_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$webgl_cmake"; webgl_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
printf '['
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
printf ',{"kind":"linux-app","status":"blocked","reason":"retained-linux-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter","prerequisites":[{"name":"retained-linux-cmake","available":%s,"detail":%s},{"name":"cmake","available":%s,"detail":"Linux retained app CMake configure/build tool"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"linux-clang"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"linux-app-output","path":%s,"pathType":"Leaf","exists":%s}]}' "$(json_bool "$linux_cmake_exists")" "$(json_string "$linux_cmake")" "$(json_bool "$cmake_exists")" "$(json_string "$linux_output")" "$(json_bool "$linux_output_exists")"
printf ',{"kind":"webgl","status":"blocked","reason":"retained-webgl-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter","prerequisites":[{"name":"retained-webgl-cmake","available":%s,"detail":%s},{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$webgl_cmake_exists")" "$(json_string "$webgl_cmake")" "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
printf ']'
}
if [ "$readiness_only" -eq 1 ]; then
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
readiness="$(package_readiness_json)"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"readiness","exitCode":0,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$elapsed_ms" "$readiness"
exit 0
fi
cmake --build --preset "$preset" --config "$configuration" --target "$target"
build_exit="$?"
if [ "$build_exit" -ne 0 ]; then
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
readiness="$(package_readiness_json)"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness"
exit "$build_exit"
fi
if [ -e "$artifact" ]; then
exit_code=0
else
exit_code=2
fi
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
readiness="$(package_readiness_json)"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness"
exit "$exit_code"

View File

@@ -0,0 +1,200 @@
[CmdletBinding()]
param(
[string[]]$Presets = @("android-arm64", "android-x64", "android-quest-arm64", "android-focus-arm64"),
[string[]]$Targets = @(
"pp_foundation",
"pp_assets",
"pp_paint",
"pp_document",
"pp_renderer_api",
"pp_renderer_gl",
"pp_paint_renderer",
"pp_ui_core",
"pp_platform_api",
"pp_app_core",
"pano_cli",
"pp_foundation_binary_stream_tests",
"pp_foundation_event_tests",
"pp_foundation_log_tests",
"pp_foundation_parse_tests",
"pp_foundation_task_queue_tests",
"pp_foundation_trace_tests",
"pp_assets_brush_package_tests",
"pp_assets_image_format_tests",
"pp_assets_image_metadata_tests",
"pp_assets_image_pixels_tests",
"pp_assets_ppi_header_tests",
"pp_assets_settings_document_tests",
"pp_paint_brush_tests",
"pp_paint_blend_tests",
"pp_paint_stroke_tests",
"pp_paint_stroke_script_tests",
"pp_document_tests",
"pp_document_ppi_import_tests",
"pp_document_ppi_export_tests",
"pp_renderer_api_tests",
"pp_renderer_gl_capabilities_tests",
"pp_renderer_gl_command_plan_tests",
"pp_paint_renderer_compositor_tests",
"pp_platform_api_tests",
"pp_ui_core_color_tests",
"pp_ui_core_layout_value_tests",
"pp_ui_core_layout_xml_tests",
"pp_ui_core_node_lifetime_tests",
"pp_ui_core_overlay_lifetime_tests",
"pp_app_core_about_menu_tests",
"pp_app_core_app_dialog_tests",
"pp_app_core_app_preferences_tests",
"pp_app_core_app_frame_tests",
"pp_app_core_app_thread_tests",
"pp_app_core_app_input_tests",
"pp_app_core_app_shutdown_tests",
"pp_app_core_app_startup_tests",
"pp_app_core_app_status_tests",
"pp_app_core_command_convert_tests",
"pp_app_core_brush_package_export_tests",
"pp_app_core_brush_package_import_tests",
"pp_app_core_brush_ui_tests",
"pp_app_core_canvas_hotkey_tests",
"pp_app_core_canvas_tool_ui_tests",
"pp_app_core_canvas_view_tests",
"pp_app_core_document_animation_tests",
"pp_app_core_document_canvas_tests",
"pp_app_core_document_cloud_tests",
"pp_app_core_document_export_tests",
"pp_app_core_document_import_tests",
"pp_app_core_document_layer_tests",
"pp_app_core_document_platform_io_tests",
"pp_app_core_document_recording_tests",
"pp_app_core_document_resize_tests",
"pp_app_core_document_route_tests",
"pp_app_core_document_sharing_tests",
"pp_app_core_document_session_tests",
"pp_app_core_file_menu_tests",
"pp_app_core_grid_ui_tests",
"pp_app_core_history_ui_tests",
"pp_app_core_main_toolbar_tests",
"pp_app_core_quick_ui_tests",
"pp_app_core_tools_menu_tests"
)
)
$ErrorActionPreference = "Stop"
function Expand-ArgumentList {
param([string[]]$Values)
$expanded = @()
foreach ($value in $Values) {
foreach ($part in ($value -split ",")) {
$trimmed = $part.Trim()
if ($trimmed.Length -gt 0) {
$expanded += $trimmed
}
}
}
return $expanded
}
function Get-VcpkgRoot {
$candidates = @()
if ($env:VCPKG_ROOT) {
$candidates += $env:VCPKG_ROOT
}
$programFiles = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ }
$vsYears = @("2026", "2022")
$vsEditions = @("Community", "Professional", "Enterprise", "BuildTools", "Preview")
foreach ($root in $programFiles) {
foreach ($year in $vsYears) {
foreach ($edition in $vsEditions) {
$candidates += (Join-Path $root "Microsoft Visual Studio\$year\$edition\VC\vcpkg")
}
}
}
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path -LiteralPath (Join-Path $candidate "vcpkg.exe") -PathType Leaf)) {
return (Resolve-Path -LiteralPath $candidate).Path
}
}
throw "VCPKG_ROOT was not set and no Visual Studio bundled vcpkg root was found."
}
function Set-VcpkgRootEnvironment {
$vcpkgRoot = Get-VcpkgRoot
$env:VCPKG_ROOT = $vcpkgRoot
return [ordered]@{
vcpkgRoot = $vcpkgRoot
vcpkgCommand = Join-Path $vcpkgRoot "vcpkg.exe"
}
}
$Presets = @(Expand-ArgumentList -Values $Presets)
$Targets = @(Expand-ArgumentList -Values $Targets)
$cmakeCommand = "cmake"
$androidToolchain = $null
$vcpkgToolchain = $null
if ($Presets | Where-Object { $_ -like "*vcpkg*" }) {
$vcpkgToolchain = Set-VcpkgRootEnvironment
}
if ($Presets | Where-Object { $_ -like "android-*" }) {
. "$PSScriptRoot\android-sdk-env.ps1"
$androidToolchain = Set-AndroidSdkToolchainEnvironment
$cmakeCommand = $androidToolchain.cmakeCommand
}
$started = Get-Date
$results = @()
$overallExitCode = 0
foreach ($preset in $Presets) {
$presetCmakeCommand = $cmakeCommand
if ($androidToolchain -and $preset -notlike "android-*") {
$presetCmakeCommand = "cmake"
}
& $presetCmakeCommand --preset $preset
$configureExitCode = $LASTEXITCODE
if ($configureExitCode -ne 0) {
$overallExitCode = $configureExitCode
$results += [ordered]@{
preset = $preset
stage = "configure"
exitCode = $configureExitCode
}
continue
}
$buildArgs = @("--build", "--preset", $preset)
foreach ($target in $Targets) {
$buildArgs += @("--target", $target)
}
& $presetCmakeCommand @buildArgs
$buildExitCode = $LASTEXITCODE
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
$overallExitCode = $buildExitCode
}
$results += [ordered]@{
preset = $preset
stage = "build"
targets = $Targets
exitCode = $buildExitCode
}
}
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "platform-build"
exitCode = $overallExitCode
elapsedMs = $elapsed
androidToolchain = $androidToolchain
vcpkgToolchain = $vcpkgToolchain
results = $results
} | ConvertTo-Json -Compress -Depth 6
exit $overallExitCode

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env sh
set -u
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests pp_app_core_about_menu_tests pp_app_core_app_dialog_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}"
start="$(date +%s)"
android_cmake_cmd=""
case " $presets " in
*" android-"*)
# shellcheck disable=SC1091
. "$(dirname "$0")/android-sdk-env.sh"
set_android_sdk_toolchain_environment || exit 1
android_cmake_cmd="$ANDROID_CMAKE_COMMAND"
;;
esac
overall_exit=0
results=""
first_result=1
build_args=""
for target in $targets; do
build_args="$build_args --target $target"
done
normalized_presets="$(printf '%s' "$presets" | tr ',' ' ')"
for preset in $normalized_presets; do
cmake_cmd="cmake"
case "$preset" in
android-*)
cmake_cmd="$android_cmake_cmd"
;;
esac
"$cmake_cmd" --preset "$preset"
configure_exit="$?"
if [ "$configure_exit" -ne 0 ]; then
[ "$overall_exit" -eq 0 ] && overall_exit="$configure_exit"
result="$(printf '{"preset":"%s","stage":"configure","exitCode":%s}' "$preset" "$configure_exit")"
else
# shellcheck disable=SC2086
"$cmake_cmd" --build --preset "$preset" $build_args
build_exit="$?"
[ "$build_exit" -ne 0 ] && [ "$overall_exit" -eq 0 ] && overall_exit="$build_exit"
result="$(printf '{"preset":"%s","stage":"build","targets":"%s","exitCode":%s}' "$preset" "$targets" "$build_exit")"
fi
if [ "$first_result" -eq 1 ]; then
results="$result"
first_result=0
else
results="$results,$result"
fi
done
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -n "${ANDROID_CMAKE_COMMAND:-}" ]; then
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"androidToolchain":{"sdkRoot":"%s","sdkManagerCommand":"%s","packageUpdates":[{"package":"ndk","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"},{"package":"cmake","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"}],"ndkVersion":"%s","ndkPath":"%s","cmakeVersion":"%s","cmakeCommand":"%s"},"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$ANDROID_SDK_ROOT" "$ANDROID_SDKMANAGER_COMMAND" "$ANDROID_NDK_INSTALLED_BEFORE" "$ANDROID_NDK_AVAILABLE_VERSION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_UPDATE_ACTION" "$ANDROID_CMAKE_INSTALLED_BEFORE" "$ANDROID_CMAKE_AVAILABLE_VERSION" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_UPDATE_ACTION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_HOME" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_COMMAND" "$results"
else
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$results"
fi
exit "$overall_exit"

View File

@@ -0,0 +1,304 @@
[CmdletBinding()]
param(
[string]$BuildPreset = "windows-msvc-default",
[string]$Configuration = "Debug",
[string[]]$BuildTargets = @("PanoPainter", "pano_cli"),
[string]$TestPreset = "desktop-fast",
[string]$TestRegex = "",
[switch]$Configure,
[switch]$SkipBuild,
[switch]$SkipTests,
[string]$CMakeCommand = "",
[string]$CTestCommand = "",
[string]$LogDir = "out/logs/quiet-validation",
[string]$IgnoreFilterFile = "",
[string[]]$IgnorePattern = @(),
[int]$FailureTailLines = 0
)
$ErrorActionPreference = "Stop"
function Resolve-CMakeCommand {
param([string]$Requested)
if ($Requested.Length -gt 0) {
return $Requested
}
$vsCmake = "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
if (Test-Path -LiteralPath $vsCmake) {
return $vsCmake
}
return "cmake"
}
function Resolve-CTestCommand {
param(
[string]$Requested,
[string]$ResolvedCMake
)
if ($Requested.Length -gt 0) {
return $Requested
}
if ($ResolvedCMake.EndsWith("cmake.exe", [System.StringComparison]::OrdinalIgnoreCase)) {
$candidate = Join-Path -Path (Split-Path -Parent $ResolvedCMake) -ChildPath "ctest.exe"
if (Test-Path -LiteralPath $candidate) {
return $candidate
}
}
return "ctest"
}
function Read-IgnorePatterns {
param(
[string]$FilterFile,
[string[]]$InlinePatterns
)
$patterns = @()
if ($FilterFile.Length -eq 0) {
$defaultFile = Join-Path -Path $PSScriptRoot -ChildPath "quiet-validation-ignore.txt"
if (Test-Path -LiteralPath $defaultFile) {
$FilterFile = $defaultFile
}
}
if ($FilterFile.Length -gt 0 -and (Test-Path -LiteralPath $FilterFile)) {
$patterns += Get-Content -LiteralPath $FilterFile |
Where-Object { $_.Trim().Length -gt 0 -and -not $_.TrimStart().StartsWith("#") }
}
$patterns += $InlinePatterns
return @($patterns | Where-Object { $_ -and $_.Length -gt 0 })
}
function Expand-ArgumentList {
param([string[]]$Values)
$expanded = @()
foreach ($value in $Values) {
if ($null -eq $value) {
continue
}
$expanded += $value -split "[,\s]+" | Where-Object { $_.Length -gt 0 }
}
return @($expanded)
}
function Limit-LogSlug {
param(
[string]$Value,
[int]$MaxLength = 96
)
if ($Value.Length -le $MaxLength) {
return $Value
}
return $Value.Substring(0, $MaxLength)
}
function Test-IgnoredLine {
param(
[string]$Line,
[string[]]$Patterns
)
foreach ($pattern in $Patterns) {
if ($Line -match $pattern) {
return $true
}
}
return $false
}
function Measure-Log {
param(
[string]$Path,
[string[]]$IgnorePatterns
)
$errorPattern = "(?i)(:\s*(fatal\s+)?error\s+[A-Z0-9]+:|^LINK\s*:\s*fatal error|^CMake Error|Errors while running CTest|Unable to find executable|\*\*\*Failed)"
$warningPattern = "(?i)(:\s*warning\s+[A-Z0-9]+:|^LINK\s*:\s*warning\s+[A-Z0-9]+:|warning:)"
$ctestSummaryPattern = "(\d+)% tests passed, (\d+) tests failed out of (\d+)"
$lineCount = 0
$rawErrors = 0
$rawWarnings = 0
$visibleErrors = 0
$visibleWarnings = 0
$ignoredErrors = 0
$ignoredWarnings = 0
$testsFailed = $null
$testsTotal = $null
if (Test-Path -LiteralPath $Path) {
foreach ($line in Get-Content -LiteralPath $Path) {
++$lineCount
$ignored = Test-IgnoredLine -Line $line -Patterns $IgnorePatterns
if ($line -match $ctestSummaryPattern) {
$testsFailed = [int]$Matches[2]
$testsTotal = [int]$Matches[3]
}
if ($line -match $errorPattern) {
++$rawErrors
if ($ignored) { ++$ignoredErrors } else { ++$visibleErrors }
}
if ($line -match $warningPattern) {
++$rawWarnings
if ($ignored) { ++$ignoredWarnings } else { ++$visibleWarnings }
}
}
}
return [ordered]@{
lineCount = $lineCount
errors = $visibleErrors
warnings = $visibleWarnings
rawErrors = $rawErrors
rawWarnings = $rawWarnings
ignoredErrors = $ignoredErrors
ignoredWarnings = $ignoredWarnings
testsFailed = $testsFailed
testsTotal = $testsTotal
}
}
function Invoke-QuietStep {
param(
[string]$Name,
[string]$Command,
[string[]]$Arguments,
[string]$LogPath,
[string[]]$IgnorePatterns,
[int]$FailureTailLines
)
$started = Get-Date
$exitCode = 0
try {
& $Command @Arguments *> $LogPath
$exitCode = $LASTEXITCODE
if ($null -eq $exitCode) {
$exitCode = 0
}
}
catch {
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
$exitCode = 1
}
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
$summary = Measure-Log -Path $LogPath -IgnorePatterns $IgnorePatterns
$result = [ordered]@{
name = $Name
exitCode = $exitCode
elapsedMs = $elapsed
log = $LogPath
summary = $summary
}
if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and (Test-Path -LiteralPath $LogPath)) {
$result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines | ForEach-Object { [string]$_ })
}
return $result
}
$resolvedCMake = Resolve-CMakeCommand -Requested $CMakeCommand
$resolvedCTest = Resolve-CTestCommand -Requested $CTestCommand -ResolvedCMake $resolvedCMake
$BuildTargets = @(Expand-ArgumentList -Values $BuildTargets)
$IgnorePattern = @(Expand-ArgumentList -Values $IgnorePattern)
$ignorePatterns = Read-IgnorePatterns -FilterFile $IgnoreFilterFile -InlinePatterns $IgnorePattern
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$runId = Get-Date -Format "yyyyMMdd-HHmmss"
$started = Get-Date
$results = @()
$overallExitCode = 0
if ($Configure) {
$log = Join-Path -Path $LogDir -ChildPath "$runId-configure-$BuildPreset.log"
$result = Invoke-QuietStep `
-Name "configure:$BuildPreset" `
-Command $resolvedCMake `
-Arguments @("--preset", $BuildPreset) `
-LogPath $log `
-IgnorePatterns $ignorePatterns `
-FailureTailLines $FailureTailLines
$results += $result
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
$overallExitCode = $result.exitCode
}
}
if (-not $SkipBuild) {
$targets = @($BuildTargets | Where-Object { $_ -and $_.Length -gt 0 })
if ($targets.Count -gt 0) {
$safeTargets = Limit-LogSlug -Value (($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
$log = Join-Path -Path $LogDir -ChildPath "$runId-build-$BuildPreset-$Configuration-$safeTargets.log"
$buildArgs = @("--build", "--preset", $BuildPreset, "--config", $Configuration, "--target") + $targets
$result = Invoke-QuietStep `
-Name ("build:{0}:{1}" -f $BuildPreset, $Configuration) `
-Command $resolvedCMake `
-Arguments $buildArgs `
-LogPath $log `
-IgnorePatterns $ignorePatterns `
-FailureTailLines $FailureTailLines
$result.targets = $targets
$results += $result
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
$overallExitCode = $result.exitCode
}
}
}
if (-not $SkipTests) {
$safeRegex = if ($TestRegex.Length -gt 0) {
Limit-LogSlug -Value ($TestRegex -replace "[^A-Za-z0-9_.-]", "_")
} else {
"all"
}
$log = Join-Path -Path $LogDir -ChildPath "$runId-test-$TestPreset-$Configuration-$safeRegex.log"
$testArgs = @("--preset", $TestPreset, "--build-config", $Configuration, "--output-on-failure")
if ($TestRegex.Length -gt 0) {
$testArgs += @("-R", $TestRegex)
}
$result = Invoke-QuietStep `
-Name ("test:{0}:{1}" -f $TestPreset, $Configuration) `
-Command $resolvedCTest `
-Arguments $testArgs `
-LogPath $log `
-IgnorePatterns $ignorePatterns `
-FailureTailLines $FailureTailLines
if ($TestRegex.Length -gt 0) {
$result.testRegex = $TestRegex
}
$results += $result
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
$overallExitCode = $result.exitCode
}
}
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
$summaryPath = Join-Path -Path $LogDir -ChildPath "$runId-summary.json"
$payload = [ordered]@{
command = "quiet-validate"
exitCode = $overallExitCode
elapsedMs = $elapsed
buildPreset = $BuildPreset
configuration = $Configuration
testPreset = $TestPreset
logDir = $LogDir
summary = $summaryPath
ignoreFilterFile = $IgnoreFilterFile
ignorePatternCount = $ignorePatterns.Count
results = $results
}
$payload | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $summaryPath -Encoding utf8
$payload | ConvertTo-Json -Compress -Depth 8
exit $overallExitCode

View 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

View File

@@ -0,0 +1,22 @@
[CmdletBinding()]
param(
[string]$Preset = "desktop-fast",
[string]$Configuration = "Debug"
)
$ErrorActionPreference = "Stop"
$started = Get-Date
& ctest --preset $Preset --build-config $Configuration
$exitCode = $LASTEXITCODE
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "test"
preset = $Preset
configuration = $Configuration
exitCode = $exitCode
elapsedMs = $elapsed
} | ConvertTo-Json -Compress
exit $exitCode

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env sh
set -u
preset="${1:-desktop-fast}"
configuration="${2:-Debug}"
start="$(date +%s)"
ctest --preset "$preset" --build-config "$configuration"
exit_code="$?"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"test","preset":"%s","configuration":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$exit_code" "$elapsed_ms"
exit "$exit_code"

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Verify package-smoke wrappers report the expected package readiness matrix."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
EXPECTED_PACKAGE_KINDS = [
"windows-appx",
"android-standard-apk",
"android-quest-apk",
"android-focus-apk",
"apple-bundle",
"linux-app",
"webgl",
]
EXPECTED_CMAKE_PACKAGE_TARGETS = [
"panopainter_package_readiness",
"panopainter_windows_app_package_smoke",
"panopainter_android_standard_native_package",
"panopainter_android_vr_native_package_configure",
"panopainter_android_native_package_smoke",
"panopainter_linux_webgl_package_readiness",
]
def repo_root() -> Path:
return Path(__file__).resolve().parents[2]
def powershell_package_kinds(root: Path) -> list[str]:
script = (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8")
match = re.search(r"\[string\[\]\]\$PackageKinds\s*=\s*@\((.*?)\n\s*\)", script, re.S)
if not match:
raise RuntimeError("Could not find PackageKinds default in package-smoke.ps1")
return sorted(re.findall(r'"([^"]+)"', match.group(1)))
def shell_package_kinds(root: Path) -> list[str]:
script = (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8")
return sorted(set(re.findall(r'"kind":"([^"]+)"', script)))
def count_regex(root: Path, patterns: dict[str, str]) -> dict[str, int]:
counts: dict[str, int] = {}
for script_name, pattern in patterns.items():
text = (root / "scripts" / "automation" / script_name).read_text(encoding="utf-8")
counts[script_name] = len(re.findall(pattern, text))
return counts
def main() -> int:
root = repo_root()
expected = sorted(EXPECTED_PACKAGE_KINDS)
ps_kinds = powershell_package_kinds(root)
sh_kinds = shell_package_kinds(root)
debt_counts = count_regex(root, {
"package-smoke.ps1": r'debt\s*=\s*"DEBT-0011"',
"package-smoke.sh": r'"debt":"DEBT-0011"',
})
blocked_counts = count_regex(root, {
"package-smoke.ps1": r'-Status\s+"blocked"',
"package-smoke.sh": r'"status":"blocked"',
})
readiness_mode_counts = {
"package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8").count("ReadinessOnly"),
"package-smoke.sh": (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8").count("readiness_only"),
}
retained_android_native_counts = count_regex(root, {
"package-smoke.ps1": r"retained-native-cmake-check",
"package-smoke.sh": r"retained-native-cmake-check",
})
retained_platform_cmake_counts = count_regex(root, {
"package-smoke.ps1": r"retained-(linux|webgl)-cmake",
"package-smoke.sh": r"retained-(linux|webgl)-cmake",
})
cmake_package_module = (root / "cmake" / "PanoPainterPackageTargets.cmake").read_text(encoding="utf-8")
root_cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
missing = {
"package-smoke.ps1": [kind for kind in expected if kind not in ps_kinds],
"package-smoke.sh": [kind for kind in expected if kind not in sh_kinds],
}
unexpected = {
"package-smoke.ps1": [kind for kind in ps_kinds if kind not in expected],
"package-smoke.sh": [kind for kind in sh_kinds if kind not in expected],
}
debt_thresholds = {
"package-smoke.ps1": 1,
"package-smoke.sh": len(expected),
}
debt_complete = {name: count >= debt_thresholds[name] for name, count in debt_counts.items()}
blocked_complete = {name: count >= len(expected) for name, count in blocked_counts.items()}
readiness_mode_present = {name: count > 0 for name, count in readiness_mode_counts.items()}
retained_android_native_complete = {
name: count >= 3 for name, count in retained_android_native_counts.items()
}
retained_platform_cmake_complete = {
name: count >= 2 for name, count in retained_platform_cmake_counts.items()
}
cmake_package_targets_present = {
target: target in cmake_package_module for target in EXPECTED_CMAKE_PACKAGE_TARGETS
}
cmake_package_module_included = "include(PanoPainterPackageTargets)" in root_cmake
ok = (
all(not values for values in missing.values())
and all(not values for values in unexpected.values())
and all(debt_complete.values())
and all(blocked_complete.values())
and all(readiness_mode_present.values())
and all(retained_android_native_complete.values())
and all(retained_platform_cmake_complete.values())
and all(cmake_package_targets_present.values())
and cmake_package_module_included
)
print(json.dumps({
"ok": ok,
"expectedPackageKinds": expected,
"packageKinds": {
"package-smoke.ps1": ps_kinds,
"package-smoke.sh": sh_kinds,
},
"missing": missing,
"unexpected": unexpected,
"debtComplete": debt_complete,
"blockedComplete": blocked_complete,
"readinessModePresent": readiness_mode_present,
"retainedAndroidNativeComplete": retained_android_native_complete,
"retainedPlatformCmakeComplete": retained_platform_cmake_complete,
"cmakePackageTargetsPresent": cmake_package_targets_present,
"cmakePackageModuleIncluded": cmake_package_module_included,
}, separators=(",", ":")))
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

View 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())

View 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
View 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:]))

File diff suppressed because it is too large Load Diff

154
src/app.h
View File

@@ -19,30 +19,38 @@
#include "node_canvas.h"
#include "node_dialog_layer_rename.h"
#include "node_progress_bar.h"
#include "node_panel_grid.h"
#include "node_panel_quick.h"
#include "node_input_box.h"
#include "node_panel_animation.h"
#include "layout.h"
#include "app_core/document_session.h"
#include "app_core/app_thread.h"
namespace pp::platform {
class PlatformServices;
struct PlatformStoragePaths;
}
class NodePanelGrid;
#if defined(__OBJC__) && defined(__IOS__)
#import <Foundation/Foundation.h>
#import "GameViewController.h"
#import "AppDelegate.h"
@class GameViewController;
@class AppDelegate;
#endif
#if defined(__OBJC__) && defined(__OSX__)
#import "main.h"
@class View;
@class AppOSX;
#endif
#ifdef __ANDROID__
#include "main.h"
struct android_app;
struct engine;
#endif
#ifdef __LINUX__
#include <GLFW/glfw3.h>
#if __LINUX__ || __WEB__
struct GLFWwindow;
#endif
struct VRController
{
enum class kButton : uint8_t
@@ -155,6 +163,7 @@ public:
float display_density = 1.f;
float zoom = 1.f;
int idle_ms = 100;
pp::platform::PlatformServices* platform_services_ = nullptr;
#if defined(__IOS__) && defined(__OBJC__)
GameViewController* ios_view;
@@ -174,15 +183,53 @@ public:
bool clipboard_set_text(const std::string& s);
void pick_image(std::function<void(std::string path)> callback);
void pick_file(std::vector<std::string> types, std::function<void(std::string path)> callback);
#if __IOS__ || __WEB__
void pick_file_save(const std::string& type, const std::string& default_name,
std::function<void(std::string path)> writer, std::function<void(const std::string& path, bool saved)> callback);
#else
void pick_file_save(std::vector<std::string> types, std::function<void(std::string path)> callback);
#endif
[[nodiscard]] bool supports_working_directory_picker() const;
[[nodiscard]] std::string format_working_directory_path(std::string_view path) const;
[[nodiscard]] bool uses_prepared_file_writes() const;
[[nodiscard]] bool uses_work_directory_document_export_collections() const;
[[nodiscard]] bool disables_network_tls_verification() const;
[[nodiscard]] bool uses_ppbr_export_data_directory_override() const;
[[nodiscard]] bool platform_supports_sonarpen() const;
void start_platform_sonarpen();
[[nodiscard]] int default_canvas_resolution() const;
[[nodiscard]] bool draws_canvas_tip_for_input(kEventSource source, kEventType type) const;
[[nodiscard]] float adjust_canvas_input_pressure(float pressure) const;
void pick_dir(std::function<void(std::string path)> callback);
void display_file(std::string path);
void share_file(std::string path);
void request_app_close();
[[nodiscard]] bool start_platform_vr_mode();
void stop_platform_vr_mode();
void attach_ui_thread();
void detach_ui_thread();
void acquire_render_context();
void release_render_context();
void present_render_context();
void bind_default_render_target();
void bind_main_render_target();
void apply_render_platform_hints();
void install_render_debug_callback();
void begin_render_capture_frame();
void end_render_capture_frame();
[[nodiscard]] bool platform_deletes_recorded_files_on_clear();
void clear_platform_recorded_files(std::string path);
void publish_exported_image(std::string path);
void flush_platform_storage();
[[nodiscard]] std::vector<std::string> document_browse_roots() const;
void save_platform_ui_state();
[[nodiscard]] bool platform_enables_live_asset_reloading();
void update_platform_frame(float delta_time_seconds);
void report_rendered_frames(int frames);
void save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> callback);
void set_platform_services(pp::platform::PlatformServices* services) noexcept;
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths();
void showKeyboard();
void hideKeyboard();
void initLog();
@@ -248,6 +295,8 @@ public:
void dialog_usermanual();
void dialog_changelog();
void dialog_about();
void save_document(pp::app::DocumentSaveIntent intent);
void continue_document_workflow_after_optional_save(std::function<void()> action);
void dialog_newdoc();
void dialog_save();
void dialog_save_ver();
@@ -327,21 +376,36 @@ public:
{
AppTask pt(task);
auto f = pt.get_future();
if (is_render_thread())
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
unique,
0U,
render_running,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
render_tasklist.size(),
render_running,
false,
false);
// remove any previously queued task from the same lambda
if (unique && !render_tasklist.empty())
if (queue_dispatch.remove_matching_unique_task)
render_tasklist.erase(std::remove_if(render_tasklist.begin(), render_tasklist.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist.end());
render_tasklist.push_back(std::move(pt));
}
render_cv.notify_all();
if (dispatch.notify_worker)
render_cv.notify_all();
}
return f;
}
@@ -351,19 +415,27 @@ public:
{
AppTask pt(task);
auto f = pt.get_future();
if (is_render_thread())
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
false,
0U,
render_running,
true,
false);
if (dispatch.execute_immediately)
{
pt();
}
else
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex);
render_tasklist.push_back(std::move(pt));
}
render_cv.notify_all();
if (dispatch.notify_worker)
render_cv.notify_all();
}
if (render_running)
if (dispatch.wait_for_completion)
f.get();
}
@@ -399,21 +471,36 @@ public:
{
AppTask pt(task);
auto f = pt.get_future();
if (is_ui_thread())
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
unique,
0U,
ui_running,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
ui_tasklist.size(),
ui_running,
false,
false);
// remove any previously queued task from the same lambda
if (unique && !ui_tasklist.empty())
if (queue_dispatch.remove_matching_unique_task)
ui_tasklist.erase(std::remove_if(ui_tasklist.begin(), ui_tasklist.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist.end());
ui_tasklist.push_back(std::move(pt));
}
ui_cv.notify_all();
if (dispatch.notify_worker)
ui_cv.notify_all();
}
return f;
}
@@ -423,21 +510,30 @@ public:
{
AppTask pt(task);
auto f = pt.get_future();
if (is_ui_thread())
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
false,
0U,
ui_running,
true,
true);
if (dispatch.execute_immediately)
{
pt();
}
else
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex);
ui_tasklist.push_back(std::move(pt));
}
ui_cv.notify_all();
if (dispatch.notify_worker)
ui_cv.notify_all();
}
if (ui_running)
if (dispatch.wait_for_completion)
f.get();
redraw = true;
if (dispatch.request_redraw)
redraw = true;
}
void ui_sync()

View File

@@ -1,48 +1,20 @@
#include "pch.h"
#include "app.h"
#include "app_core/document_cloud.h"
#include "legacy_cloud_services.h"
#include "util.h"
#include "node_progress_bar.h"
#include "node_dialog_cloud.h"
void App::cloud_upload()
{
if (!canvas)
return;
if (Canvas::I->m_newdoc)
{
message_box("Warning", "This document needs to be saved before upload.");
}
else
{
auto upload_thread = [this] {
BT_SetTerminate();
const bool has_canvas = canvas != nullptr;
const auto plan = pp::app::plan_cloud_upload(
has_canvas,
has_canvas && Canvas::I->m_newdoc,
has_canvas && Canvas::I->m_unsaved);
if (Canvas::I->m_unsaved)
{
Canvas::I->project_save_thread(doc_path, true);
}
auto pb = show_progress("Uploading");
upload(doc_path, doc_filename, [this,pb](float p){
pb->set_progress(p);
});
pb->destroy();
message_box("Success", "This document has been succesfully uploaded.");
};
auto m = message_box("Publish document", "Would you like to upload to the public domain?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, upload_thread](Node*) {
std::thread(upload_thread).detach();
m->destroy();
};
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
m->destroy();
};
}
const auto status = pp::panopainter::execute_legacy_cloud_upload_plan(*this, plan);
if (!status.ok())
LOG("Cloud upload action failed: %s", status.message);
}
void App::cloud_upload_all()
@@ -51,70 +23,18 @@ void App::cloud_upload_all()
BT_SetTerminate();
auto names = Asset::list_files(data_path, ".*\\.ppi");
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
gl_state gl;
std::shared_ptr<NodeProgressBar> pb;
if (layout.m_loaded)
pb = show_progress("Export Pano Image", names.size());
for (const auto& n : names)
{
std::string path = data_path + "/" + n;
upload(path);
if (layout.m_loaded)
pb->increment();
}
if (layout.m_loaded)
pb->destroy();
const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
if (!status.ok())
LOG("Cloud bulk upload action failed: %s", status.message);
}).detach();
}
void App::cloud_browse()
{
if (!canvas)
return;
// load thumbnail test
auto dialog = std::make_shared<NodeDialogCloud>();
dialog->set_manager(&layout);
dialog->init();
dialog->create();
dialog->loaded();
layout[main_id]->add_child(dialog);
dialog->btn_ok->on_click = [this, dialog](Node*)
{
if (dialog->selected_file.empty())
return;
dialog->destroy();
std::thread([this, dialog] {
BT_SetTerminate();
auto* m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Downloading");
m->m_message->set_text("Download in progress");
std::string url = "https://panopainter.com/cloud/cloud-dwl.php?file=" + dialog->selected_file;
download(url, dialog->selected_path, [this,m](float p){
static char progress[256];
sprintf(progress, "Download in progress %.2f%%", p * 100.f);
m->m_message->set_text(progress);
});
canvas->reset_camera();
layers->clear();
canvas->m_canvas->project_open_thread(dialog->selected_path);
doc_name = dialog->selected_name;
title_update();
for (auto& l : canvas->m_canvas->m_layers)
layers->add_layer(l->m_name.c_str(), false);
ActionManager::clear();
m->destroy();
}).detach();
};
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
const auto status = pp::panopainter::execute_legacy_cloud_browse_action(*this, browse_plan);
if (!status.ok())
LOG("Cloud browse action failed: %s", status.message);
}

View File

@@ -1,16 +1,70 @@
#include "pch.h"
#include "app_core/command_convert.h"
#include "app.h"
#include "canvas.h"
#include "legacy_ui_gl_dispatch.h"
#include "log.h"
#include "renderer_gl/opengl_capabilities.h"
namespace {
void apply_convert_command_state()
{
const auto status = pp::renderer::gl::apply_panopainter_convert_command_state(
pp::renderer::gl::OpenGlConvertCommandStateDispatch {
.enable = pp::legacy::ui_gl::enable_opengl_state,
.disable = pp::legacy::ui_gl::disable_opengl_state,
.blend_func = pp::legacy::ui_gl::set_opengl_blend_func,
.blend_equation = pp::legacy::ui_gl::set_opengl_blend_equation,
});
if (!status.ok())
LOG("OpenGL convert command state failed: %s", status.message);
}
class LegacyCommandConvertServices final : public pp::app::CommandConvertServices {
public:
void apply_renderer_state() override
{
apply_convert_command_state();
}
void create_canvas(int canvas_resolution) override
{
command_canvas = new Canvas;
command_canvas->create(canvas_resolution, canvas_resolution);
}
void open_project(std::string_view project_path) override
{
if (command_canvas)
command_canvas->project_open_thread(std::string(project_path));
}
void export_equirectangular(std::string_view output_path) override
{
if (command_canvas)
command_canvas->export_equirectangular_thread(std::string(output_path));
}
private:
Canvas* command_canvas = nullptr;
};
}
void App::cmd_convert(std::string pano_path, std::string out_path)
{
glDisable(GL_DEPTH_TEST);
glEnable(GL_PROGRAM_POINT_SIZE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_ADD);
const auto plan = pp::app::plan_command_convert(
pano_path,
out_path,
default_canvas_resolution());
if (!plan) {
LOG("Convert command rejected: %s", plan.status().message);
return;
}
Canvas* canvas = new Canvas;
canvas->create(CANVAS_RES, CANVAS_RES);
canvas->project_open_thread(pano_path);
canvas->export_equirectangular_thread(out_path);
LegacyCommandConvertServices services;
const auto status = pp::app::execute_command_convert_plan(plan.value(), services);
if (!status.ok())
LOG("Convert command failed: %s", status.message);
}

126
src/app_core/about_menu.h Normal file
View File

@@ -0,0 +1,126 @@
#pragma once
#include "foundation/result.h"
#include <string>
namespace pp::app {
enum class AboutMenuCommand {
help_guide,
about_app,
whats_new,
induce_crash,
performance_test,
};
enum class AboutMenuAction {
show_user_manual,
show_about_dialog,
show_whats_new_dialog,
trigger_crash_test,
run_performance_test,
no_op_unavailable,
};
struct AboutMenuPlan {
AboutMenuCommand command = AboutMenuCommand::help_guide;
AboutMenuAction action = AboutMenuAction::show_user_manual;
std::string label;
bool closes_root_popup = true;
bool requires_canvas = false;
int performance_iterations = 0;
int performance_updates_per_iteration = 0;
};
class AboutMenuServices {
public:
virtual ~AboutMenuServices() = default;
virtual void show_user_manual() = 0;
virtual void show_about_dialog() = 0;
virtual void show_whats_new_dialog() = 0;
virtual void trigger_crash_test() = 0;
virtual void run_performance_test(const AboutMenuPlan& plan) = 0;
};
[[nodiscard]] inline AboutMenuPlan plan_about_menu_command(
AboutMenuCommand command,
int version_major,
int version_minor,
int version_fix,
bool diagnostics_available = true,
bool has_canvas = true)
{
AboutMenuPlan plan;
plan.command = command;
switch (command) {
case AboutMenuCommand::help_guide:
plan.action = AboutMenuAction::show_user_manual;
plan.label = "Help Guide";
break;
case AboutMenuCommand::about_app:
plan.action = AboutMenuAction::show_about_dialog;
plan.label = "About PanoPainter";
break;
case AboutMenuCommand::whats_new:
plan.action = AboutMenuAction::show_whats_new_dialog;
plan.label = "What's new in "
+ std::to_string(version_major)
+ "."
+ std::to_string(version_minor)
+ "."
+ std::to_string(version_fix)
+ "?";
break;
case AboutMenuCommand::induce_crash:
plan.label = "Induce crash";
plan.action = diagnostics_available
? AboutMenuAction::trigger_crash_test
: AboutMenuAction::no_op_unavailable;
plan.closes_root_popup = diagnostics_available;
break;
case AboutMenuCommand::performance_test:
plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)";
plan.requires_canvas = true;
plan.performance_iterations = 100;
plan.performance_updates_per_iteration = 10;
plan.action = has_canvas
? AboutMenuAction::run_performance_test
: AboutMenuAction::no_op_unavailable;
plan.closes_root_popup = has_canvas;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_about_menu_plan(
const AboutMenuPlan& plan,
AboutMenuServices& services)
{
switch (plan.action) {
case AboutMenuAction::show_user_manual:
services.show_user_manual();
return pp::foundation::Status::success();
case AboutMenuAction::show_about_dialog:
services.show_about_dialog();
return pp::foundation::Status::success();
case AboutMenuAction::show_whats_new_dialog:
services.show_whats_new_dialog();
return pp::foundation::Status::success();
case AboutMenuAction::trigger_crash_test:
services.trigger_crash_test();
return pp::foundation::Status::success();
case AboutMenuAction::run_performance_test:
services.run_performance_test(plan);
return pp::foundation::Status::success();
case AboutMenuAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown about menu action");
}
}

82
src/app_core/app_dialog.h Normal file
View File

@@ -0,0 +1,82 @@
#pragma once
#include "foundation/result.h"
#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";
};
[[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
View 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
View 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

View File

@@ -0,0 +1,195 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <span>
namespace pp::app {
enum class InterfaceDirection {
left_to_right,
right_to_left,
};
enum class TimelapseRecordingAction {
no_op,
start_recording,
stop_recording,
};
struct ScaleApplicationPlan {
float scale = 1.0F;
float display_density = 1.0F;
float font_scale = 1.0F;
};
struct ScaleOptionSelection {
bool has_selection = false;
std::size_t index = 0;
};
struct InterfaceDirectionPlan {
InterfaceDirection direction = InterfaceDirection::left_to_right;
};
struct TimelapsePreferencePlan {
bool enabled = true;
TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op;
};
struct StoredIntegerPreferencePlan {
int value = 0;
};
struct StoredBooleanPreferencePlan {
bool value = false;
};
class AppPreferenceServices {
public:
virtual ~AppPreferenceServices() = default;
virtual void apply_ui_scale(const ScaleApplicationPlan& plan) = 0;
virtual void apply_viewport_scale(const ScaleApplicationPlan& plan) = 0;
virtual void apply_interface_direction(const InterfaceDirectionPlan& plan) = 0;
virtual bool apply_vr_mode_preference(const StoredBooleanPreferencePlan& plan) = 0;
virtual void apply_vr_controllers_preference(const StoredBooleanPreferencePlan& plan) = 0;
virtual void apply_timelapse_preference(const TimelapsePreferencePlan& plan) = 0;
virtual void apply_canvas_cursor_mode(const StoredIntegerPreferencePlan& plan) = 0;
};
[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale(
float requested_scale,
float display_density) noexcept
{
return {
requested_scale,
display_density,
requested_scale * display_density,
};
}
[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale(
float requested_scale,
float display_density = 1.0F) noexcept
{
return {
requested_scale,
display_density,
requested_scale * display_density,
};
}
[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection(
float current_scale,
std::span<const float> options) noexcept
{
ScaleOptionSelection selection;
for (std::size_t index = 0; index < options.size(); ++index) {
if (current_scale >= options[index]) {
selection.has_selection = true;
selection.index = index;
}
}
return selection;
}
[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept
{
return {
right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right,
};
}
[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference(
bool enabled,
bool recording_running) noexcept
{
if (enabled && !recording_running) {
return { enabled, TimelapseRecordingAction::start_recording };
}
if (!enabled && recording_running) {
return { enabled, TimelapseRecordingAction::stop_recording };
}
return { enabled, TimelapseRecordingAction::no_op };
}
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference(
bool enabled) noexcept
{
return { enabled };
}
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_mode_preference(bool enabled) noexcept
{
return { enabled };
}
[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
{
return { mode };
}
[[nodiscard]] inline pp::foundation::Status execute_ui_scale_preference(
float requested_scale,
float display_density,
AppPreferenceServices& services)
{
services.apply_ui_scale(plan_ui_scale(requested_scale, display_density));
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_viewport_scale_preference(
float requested_scale,
float display_density,
AppPreferenceServices& services)
{
services.apply_viewport_scale(plan_viewport_scale(requested_scale, display_density));
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_interface_direction_preference(
bool right_to_left,
AppPreferenceServices& services)
{
services.apply_interface_direction(plan_interface_direction(right_to_left));
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_vr_mode_preference(
bool enabled,
AppPreferenceServices& services)
{
if (!services.apply_vr_mode_preference(plan_vr_mode_preference(enabled))) {
return pp::foundation::Status::invalid_argument("VR mode could not start");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_vr_controllers_preference(
bool enabled,
AppPreferenceServices& services)
{
services.apply_vr_controllers_preference(plan_vr_controllers_preference(enabled));
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_timelapse_preference(
bool enabled,
bool recording_running,
AppPreferenceServices& services)
{
services.apply_timelapse_preference(plan_timelapse_preference(enabled, recording_running));
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_canvas_cursor_mode_preference(
int mode,
AppPreferenceServices& services)
{
services.apply_canvas_cursor_mode(plan_canvas_cursor_mode(mode));
return pp::foundation::Status::success();
}
}

View 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
View File

@@ -0,0 +1,188 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <limits>
namespace pp::app {
struct AppStartupPlan {
int previous_run_counter = 0;
int next_run_counter = 1;
bool save_preferences = true;
bool start_timelapse = false;
bool vr_controllers_enabled = true;
bool show_license_warning = false;
};
struct AppStartupResourcePlan {
int ui_render_target_width = 0;
int ui_render_target_height = 0;
bool initialize_shaders = true;
bool initialize_assets = true;
bool initialize_layout = true;
bool update_title = true;
bool create_ui_render_target = true;
};
class AppStartupServices {
public:
virtual ~AppStartupServices() = default;
virtual void store_run_counter(int value) = 0;
virtual void save_preferences() = 0;
virtual void start_timelapse_recording() = 0;
virtual void apply_vr_controllers_enabled(bool enabled) = 0;
virtual void show_license_warning() = 0;
};
class AppStartupResourceServices {
public:
virtual ~AppStartupResourceServices() = default;
virtual void initialize_shaders() = 0;
virtual void initialize_assets() = 0;
virtual void initialize_layout() = 0;
virtual void update_title() = 0;
virtual void create_ui_render_target(int width, int height) = 0;
};
[[nodiscard]] inline pp::foundation::Result<AppStartupPlan> plan_app_startup(
int current_run_counter,
bool auto_timelapse_enabled,
bool stored_vr_controllers_enabled,
bool license_valid)
{
if (current_run_counter < 0) {
return pp::foundation::Result<AppStartupPlan>::failure(
pp::foundation::Status::invalid_argument("run counter must not be negative"));
}
if (current_run_counter == std::numeric_limits<int>::max()) {
return pp::foundation::Result<AppStartupPlan>::failure(
pp::foundation::Status::out_of_range("run counter would overflow"));
}
AppStartupPlan plan;
plan.previous_run_counter = current_run_counter;
plan.next_run_counter = current_run_counter + 1;
plan.start_timelapse = auto_timelapse_enabled;
plan.vr_controllers_enabled = stored_vr_controllers_enabled;
plan.show_license_warning = !license_valid;
return pp::foundation::Result<AppStartupPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<AppStartupResourcePlan> plan_app_startup_resources(
float ui_width,
float ui_height)
{
if (!std::isfinite(ui_width) || !std::isfinite(ui_height)) {
return pp::foundation::Result<AppStartupResourcePlan>::failure(
pp::foundation::Status::invalid_argument("startup resource dimensions must be finite"));
}
if (ui_width < 1.0F || ui_height < 1.0F) {
return pp::foundation::Result<AppStartupResourcePlan>::failure(
pp::foundation::Status::invalid_argument("startup resource dimensions must be positive"));
}
if (ui_width > static_cast<float>(std::numeric_limits<int>::max())
|| ui_height > static_cast<float>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<AppStartupResourcePlan>::failure(
pp::foundation::Status::out_of_range("startup resource dimensions exceed integer range"));
}
AppStartupResourcePlan plan;
plan.ui_render_target_width = static_cast<int>(ui_width);
plan.ui_render_target_height = static_cast<int>(ui_height);
return pp::foundation::Result<AppStartupResourcePlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
services.store_run_counter(plan.next_run_counter);
if (plan.save_preferences) {
services.save_preferences();
}
if (plan.start_timelapse) {
services.start_timelapse_recording();
}
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
if (plan.show_license_warning) {
services.show_license_warning();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_persistence_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
services.store_run_counter(plan.next_run_counter);
if (plan.save_preferences) {
services.save_preferences();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_runtime_plan(
const AppStartupPlan& plan,
AppStartupServices& services)
{
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
}
if (plan.start_timelapse) {
services.start_timelapse_recording();
}
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
if (plan.show_license_warning) {
services.show_license_warning();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_app_startup_resources(
const AppStartupResourcePlan& plan,
AppStartupResourceServices& services)
{
if (plan.create_ui_render_target
&& (plan.ui_render_target_width <= 0 || plan.ui_render_target_height <= 0)) {
return pp::foundation::Status::invalid_argument("startup resource plan has invalid UI render target size");
}
if (plan.initialize_shaders) {
services.initialize_shaders();
}
if (plan.initialize_assets) {
services.initialize_assets();
}
if (plan.initialize_layout) {
services.initialize_layout();
}
if (plan.update_title) {
services.update_title();
}
if (plan.create_ui_render_target) {
services.create_ui_render_target(plan.ui_render_target_width, plan.ui_render_target_height);
}
return pp::foundation::Status::success();
}
} // namespace pp::app

170
src/app_core/app_status.h Normal file
View File

@@ -0,0 +1,170 @@
#pragma once
#include "foundation/result.h"
#include <array>
#include <cstdio>
#include <cstddef>
#include <string>
#include <string_view>
namespace pp::app {
inline constexpr std::array<int, 6> document_resolution_values {
512,
1024,
1536,
2048,
4096,
8192,
};
inline constexpr std::array<std::string_view, 6> document_resolution_labels {
"2K",
"4K",
"6K",
"8K",
"16K",
"32K",
};
struct RecordingFrameLabel {
bool visible = false;
std::string text;
};
struct RendererDiagnosticsInput {
bool framebuffer_fetch = false;
bool float32_render_targets = false;
bool float32_linear_filtering = false;
bool float16_render_targets = false;
};
struct RendererDiagnosticIndicator {
bool supported = false;
std::string_view label;
};
struct RendererDiagnosticsPlan {
RendererDiagnosticIndicator framebuffer_fetch;
RendererDiagnosticIndicator floating_point_targets;
};
[[nodiscard]] inline pp::foundation::Result<int> display_resolution_from_index(int index)
{
if (index < 0 || static_cast<std::size_t>(index) >= document_resolution_values.size()) {
return pp::foundation::Result<int>::failure(
pp::foundation::Status::out_of_range("document resolution index is out of range"));
}
return pp::foundation::Result<int>::success(
document_resolution_values[static_cast<std::size_t>(index)]);
}
[[nodiscard]] inline pp::foundation::Result<std::size_t> document_resolution_to_index(int resolution)
{
for (std::size_t index = 0; index < document_resolution_values.size(); ++index) {
if (document_resolution_values[index] == resolution) {
return pp::foundation::Result<std::size_t>::success(index);
}
}
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("document resolution is not supported"));
}
[[nodiscard]] inline pp::foundation::Result<std::string_view> document_resolution_label(int resolution)
{
const auto index = document_resolution_to_index(resolution);
if (!index) {
return pp::foundation::Result<std::string_view>::failure(index.status());
}
return pp::foundation::Result<std::string_view>::success(document_resolution_labels[index.value()]);
}
[[nodiscard]] inline std::string make_document_title(
std::string_view document_name,
bool has_unsaved_changes,
int resolution)
{
const auto label = document_resolution_label(resolution);
const auto resolution_label = label ? label.value() : std::string_view("unknown");
std::string title = "Panodoc: ";
title.append(document_name);
if (has_unsaved_changes) {
title.push_back('*');
}
title.append(" (");
title.append(resolution_label);
title.push_back(')');
return title;
}
[[nodiscard]] inline std::string make_dpi_label(float zoom)
{
char buffer[64] {};
std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom);
return buffer;
}
[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes)
{
char buffer[128] {};
std::snprintf(
buffer,
sizeof(buffer),
"History memory: %.2f Mb",
static_cast<double>(bytes) / 1024.0 / 1024.0);
return buffer;
}
[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label(
bool is_recording,
bool encoder_available,
int encoded_frames)
{
if (!is_recording || !encoder_available) {
return {};
}
char buffer[128] {};
std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames);
return {
true,
buffer,
};
}
[[nodiscard]] inline RendererDiagnosticsPlan plan_renderer_diagnostics(
RendererDiagnosticsInput input) noexcept
{
RendererDiagnosticsPlan plan;
plan.framebuffer_fetch = {
input.framebuffer_fetch,
"FBF",
};
if (input.float32_linear_filtering) {
plan.floating_point_targets = {
true,
"F32L",
};
} else if (input.float32_render_targets) {
plan.floating_point_targets = {
true,
"F32",
};
} else if (input.float16_render_targets) {
plan.floating_point_targets = {
true,
"F16",
};
} else {
plan.floating_point_targets = {
false,
"",
};
}
return plan;
}
}

200
src/app_core/app_thread.h Normal file
View File

@@ -0,0 +1,200 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <cstddef>
namespace pp::app {
struct AppTaskDispatchPlan {
bool execute_immediately = false;
bool queue_task = false;
bool remove_matching_unique_task = false;
bool notify_worker = false;
bool wait_for_completion = false;
bool request_redraw = false;
};
struct AppAsyncRedrawPlan {
bool set_redraw = true;
bool notify_ui = true;
};
struct AppQueueDrainPlan {
bool mark_running = true;
bool drain_tasks = false;
bool wrap_in_render_context = false;
std::size_t task_count = 0;
};
struct AppUiTickPlan {
bool mark_running = true;
bool execute_tasks = false;
bool tick_app = true;
bool update_before_render = false;
bool enqueue_render_frame = false;
std::size_t task_count = 0;
};
struct AppUiLoopTimerPlan {
bool update_platform_frame = true;
float frame_accumulator = 0.0F;
float fps_accumulator = 0.0F;
float reload_accumulator = 0.0F;
bool report_rendered_frames = false;
int reported_frame_count = 0;
int rendered_frames_after_report = 0;
bool check_live_asset_reload = false;
};
struct AppUiLoopRedrawPlan {
bool tick_app = true;
bool update_before_render = false;
bool enqueue_render_frame = false;
bool reset_frame_accumulator = false;
int rendered_frames = 0;
};
struct AppThreadStartPlan {
bool start_thread = true;
bool mark_running = true;
};
struct AppThreadStopPlan {
bool mark_not_running = true;
bool notify_worker = true;
bool join_thread = false;
};
[[nodiscard]] constexpr AppTaskDispatchPlan plan_app_task_dispatch(
bool already_on_target_thread,
bool unique,
std::size_t queued_task_count,
bool worker_running,
bool wait_for_completion,
bool request_redraw_after_dispatch) noexcept
{
const bool queue_task = !already_on_target_thread;
return AppTaskDispatchPlan {
.execute_immediately = already_on_target_thread,
.queue_task = queue_task,
.remove_matching_unique_task = queue_task && unique && queued_task_count > 0U,
.notify_worker = queue_task,
.wait_for_completion = queue_task && worker_running && wait_for_completion,
.request_redraw = request_redraw_after_dispatch,
};
}
[[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept
{
return AppAsyncRedrawPlan {};
}
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_render_queue_drain(std::size_t queued_task_count) noexcept
{
const bool drain = queued_task_count > 0U;
return AppQueueDrainPlan {
.mark_running = true,
.drain_tasks = drain,
.wrap_in_render_context = drain,
.task_count = queued_task_count,
};
}
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_ui_queue_drain(std::size_t queued_task_count) noexcept
{
return AppQueueDrainPlan {
.mark_running = true,
.drain_tasks = queued_task_count > 0U,
.wrap_in_render_context = false,
.task_count = queued_task_count,
};
}
[[nodiscard]] constexpr AppUiTickPlan plan_app_ui_thread_tick(
std::size_t queued_task_count,
bool redraw) noexcept
{
return AppUiTickPlan {
.mark_running = true,
.execute_tasks = queued_task_count > 0U,
.tick_app = true,
.update_before_render = redraw,
.enqueue_render_frame = redraw,
.task_count = queued_task_count,
};
}
[[nodiscard]] inline pp::foundation::Result<AppUiLoopTimerPlan> plan_app_ui_loop_timers(
float delta_time_seconds,
float frame_accumulator,
float fps_accumulator,
float reload_accumulator,
int rendered_frames,
bool live_asset_reloading_enabled)
{
if (!std::isfinite(delta_time_seconds) || !std::isfinite(frame_accumulator)
|| !std::isfinite(fps_accumulator) || !std::isfinite(reload_accumulator)) {
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
pp::foundation::Status::invalid_argument("UI loop timer values must be finite"));
}
if (delta_time_seconds < 0.0F || frame_accumulator < 0.0F
|| fps_accumulator < 0.0F || reload_accumulator < 0.0F || rendered_frames < 0) {
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
pp::foundation::Status::invalid_argument("UI loop timer values must not be negative"));
}
AppUiLoopTimerPlan plan;
plan.frame_accumulator = frame_accumulator + delta_time_seconds;
plan.fps_accumulator = fps_accumulator + delta_time_seconds;
plan.reload_accumulator = reload_accumulator;
plan.rendered_frames_after_report = rendered_frames;
if (plan.fps_accumulator > 1.0F) {
plan.report_rendered_frames = true;
plan.reported_frame_count = rendered_frames;
plan.fps_accumulator = 0.0F;
plan.rendered_frames_after_report = 0;
}
if (live_asset_reloading_enabled) {
plan.reload_accumulator += delta_time_seconds;
if (plan.reload_accumulator > 1.0F) {
plan.reload_accumulator = 0.0F;
plan.check_live_asset_reload = true;
}
}
return pp::foundation::Result<AppUiLoopTimerPlan>::success(plan);
}
[[nodiscard]] constexpr AppUiLoopRedrawPlan plan_app_ui_loop_redraw(
bool redraw,
int rendered_frames) noexcept
{
return AppUiLoopRedrawPlan {
.tick_app = true,
.update_before_render = redraw,
.enqueue_render_frame = redraw,
.reset_frame_accumulator = redraw,
.rendered_frames = rendered_frames + (redraw ? 1 : 0),
};
}
[[nodiscard]] constexpr AppThreadStartPlan plan_app_thread_start() noexcept
{
return AppThreadStartPlan {};
}
[[nodiscard]] constexpr AppThreadStopPlan plan_app_thread_stop(bool thread_joinable) noexcept
{
return AppThreadStopPlan {
.mark_not_running = true,
.notify_worker = true,
.join_thread = thread_joinable,
};
}
} // namespace pp::app

View 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

View File

@@ -0,0 +1,56 @@
#pragma once
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class BrushPackageImportKind {
abr,
ppbr,
};
class BrushPackageImportServices {
public:
virtual ~BrushPackageImportServices() = default;
virtual void import_brush_package(BrushPackageImportKind kind, std::string_view path) = 0;
};
[[nodiscard]] inline const char* brush_package_import_kind_name(BrushPackageImportKind kind) noexcept
{
switch (kind) {
case BrushPackageImportKind::abr:
return "abr";
case BrushPackageImportKind::ppbr:
return "ppbr";
}
return "abr";
}
[[nodiscard]] inline pp::foundation::Status validate_brush_package_import_path(std::string_view path) noexcept
{
if (path.empty()) {
return pp::foundation::Status::invalid_argument("brush package import path must not be empty");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_brush_package_import(
BrushPackageImportKind kind,
std::string_view path,
BrushPackageImportServices& services)
{
const auto status = validate_brush_package_import_path(path);
if (!status.ok()) {
return status;
}
services.import_brush_package(kind, path);
return pp::foundation::Status::success();
}
} // namespace pp::app

1008
src/app_core/brush_ui.h Normal file

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -0,0 +1,315 @@
#pragma once
#include "foundation/result.h"
#include <array>
#include <cmath>
#include <string_view>
namespace pp::app {
enum class CanvasToolOperation {
select_mode,
toggle_picking,
toggle_touch_lock,
};
enum class CanvasToolMode {
draw,
erase,
line,
camera,
grid,
copy,
cut,
fill,
mask_free,
mask_line,
flood_fill,
};
enum class CanvasToolTransformAction {
none,
copy,
cut,
};
enum class CanvasToolToolbarAction {
select_mode,
toggle_picking,
toggle_touch_lock,
};
enum class CanvasCursorVisibilityMode {
never,
small_brush,
not_painting,
always,
};
struct CanvasToolPlan {
CanvasToolOperation operation = CanvasToolOperation::select_mode;
CanvasToolMode mode = CanvasToolMode::draw;
CanvasToolTransformAction transform_action = CanvasToolTransformAction::none;
bool selects_toolbar_button = false;
bool updates_canvas_mode = false;
bool toggles_picking = false;
bool toggles_touch_lock = false;
bool requires_draw_mode = false;
bool no_op = false;
};
struct CanvasToolButtonState {
CanvasToolMode mode = CanvasToolMode::draw;
bool pick_active = false;
bool touch_lock_active = false;
bool pen_active = false;
bool erase_active = false;
bool line_active = false;
bool camera_active = false;
bool grid_active = false;
bool copy_active = false;
bool cut_active = false;
bool fill_active = false;
bool mask_free_active = false;
bool mask_line_active = false;
bool flood_fill_active = false;
};
struct CanvasToolToolbarBinding {
std::string_view button_id;
CanvasToolToolbarAction action = CanvasToolToolbarAction::select_mode;
CanvasToolMode mode = CanvasToolMode::draw;
bool custom_button = true;
bool applies_default_on_init = false;
};
struct CanvasToolToolbarPlan {
std::array<CanvasToolToolbarBinding, 13> bindings {};
CanvasToolMode default_mode = CanvasToolMode::draw;
};
struct CanvasCursorVisibilityInput {
CanvasToolMode mode = CanvasToolMode::draw;
CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never;
bool has_current_brush = true;
float brush_tip_size = 0.0F;
bool pen_is_drawing = false;
bool alt_down = false;
bool pen_is_resizing = false;
bool pen_is_picking = false;
};
struct CanvasCursorVisibilityPlan {
bool visible = true;
bool paint_mode = false;
bool uses_brush_size = false;
bool uses_pen_state = false;
bool forced_visible_by_modifier_or_tool = false;
};
class CanvasToolServices {
public:
virtual ~CanvasToolServices() = default;
virtual void select_toolbar_button(CanvasToolMode mode) = 0;
virtual void set_transform_action(CanvasToolTransformAction action) = 0;
virtual void set_canvas_mode(CanvasToolMode mode) = 0;
virtual void toggle_picking() = 0;
virtual void toggle_touch_lock() = 0;
};
[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept
{
if (mode == CanvasToolMode::copy) {
return CanvasToolTransformAction::copy;
}
if (mode == CanvasToolMode::cut) {
return CanvasToolTransformAction::cut;
}
return CanvasToolTransformAction::none;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::select_mode;
plan.mode = mode;
plan.transform_action = transform_action_for_mode(mode);
plan.selects_toolbar_button = true;
plan.updates_canvas_mode = true;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::toggle_picking;
plan.mode = CanvasToolMode::draw;
plan.requires_draw_mode = true;
plan.toggles_picking = current_mode_is_draw;
plan.no_op = !current_mode_is_draw;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::toggle_touch_lock;
plan.toggles_touch_lock = true;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolToolbarPlan plan_canvas_tool_toolbar() noexcept
{
return {
std::array<CanvasToolToolbarBinding, 13> {
CanvasToolToolbarBinding { "btn-pen", CanvasToolToolbarAction::select_mode, CanvasToolMode::draw, true, true },
CanvasToolToolbarBinding { "btn-pick", CanvasToolToolbarAction::toggle_picking, CanvasToolMode::draw, true, false },
CanvasToolToolbarBinding { "btn-touchlock", CanvasToolToolbarAction::toggle_touch_lock, CanvasToolMode::draw, true, false },
CanvasToolToolbarBinding { "btn-erase", CanvasToolToolbarAction::select_mode, CanvasToolMode::erase, true, false },
CanvasToolToolbarBinding { "btn-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::line, true, false },
CanvasToolToolbarBinding { "btn-cam", CanvasToolToolbarAction::select_mode, CanvasToolMode::camera, false, false },
CanvasToolToolbarBinding { "btn-grid", CanvasToolToolbarAction::select_mode, CanvasToolMode::grid, false, false },
CanvasToolToolbarBinding { "btn-copy", CanvasToolToolbarAction::select_mode, CanvasToolMode::copy, false, false },
CanvasToolToolbarBinding { "btn-cut", CanvasToolToolbarAction::select_mode, CanvasToolMode::cut, false, false },
CanvasToolToolbarBinding { "btn-fill", CanvasToolToolbarAction::select_mode, CanvasToolMode::fill, false, false },
CanvasToolToolbarBinding { "btn-mask-free", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_free, true, false },
CanvasToolToolbarBinding { "btn-mask-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_line, true, false },
CanvasToolToolbarBinding { "btn-bucket", CanvasToolToolbarAction::select_mode, CanvasToolMode::flood_fill, true, false },
},
CanvasToolMode::draw,
};
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_toolbar_binding_action(
const CanvasToolToolbarBinding& binding,
bool current_mode_is_draw) noexcept
{
switch (binding.action) {
case CanvasToolToolbarAction::select_mode:
return plan_canvas_tool_select(binding.mode);
case CanvasToolToolbarAction::toggle_picking:
return plan_canvas_tool_pick_toggle(current_mode_is_draw);
case CanvasToolToolbarAction::toggle_touch_lock:
return plan_canvas_tool_touch_lock_toggle();
}
return plan_canvas_tool_select(CanvasToolMode::draw);
}
[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state(
CanvasToolMode mode,
bool picking,
bool touch_lock) noexcept
{
CanvasToolButtonState state;
state.mode = mode;
state.pick_active = mode == CanvasToolMode::draw && picking;
state.touch_lock_active = touch_lock;
state.pen_active = mode == CanvasToolMode::draw;
state.erase_active = mode == CanvasToolMode::erase;
state.line_active = mode == CanvasToolMode::line;
state.camera_active = mode == CanvasToolMode::camera;
state.grid_active = mode == CanvasToolMode::grid;
state.copy_active = mode == CanvasToolMode::copy;
state.cut_active = mode == CanvasToolMode::cut;
state.fill_active = mode == CanvasToolMode::fill;
state.mask_free_active = mode == CanvasToolMode::mask_free;
state.mask_line_active = mode == CanvasToolMode::mask_line;
state.flood_fill_active = mode == CanvasToolMode::flood_fill;
return state;
}
[[nodiscard]] inline constexpr bool canvas_tool_mode_is_paint(CanvasToolMode mode) noexcept
{
return mode == CanvasToolMode::draw || mode == CanvasToolMode::erase;
}
[[nodiscard]] inline pp::foundation::Result<CanvasCursorVisibilityPlan> plan_canvas_cursor_visibility(
const CanvasCursorVisibilityInput& input)
{
CanvasCursorVisibilityPlan plan;
plan.paint_mode = canvas_tool_mode_is_paint(input.mode);
if (!plan.paint_mode) {
plan.visible = true;
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
}
switch (input.visibility_mode) {
case CanvasCursorVisibilityMode::always:
plan.visible = true;
break;
case CanvasCursorVisibilityMode::never:
plan.visible = false;
break;
case CanvasCursorVisibilityMode::small_brush:
if (!input.has_current_brush) {
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
pp::foundation::Status::invalid_argument("canvas cursor small-brush mode requires a current brush"));
}
if (!std::isfinite(input.brush_tip_size) || input.brush_tip_size < 0.0F) {
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
pp::foundation::Status::invalid_argument("canvas cursor brush size must be finite and non-negative"));
}
plan.visible = input.brush_tip_size < 10.0F;
plan.uses_brush_size = true;
break;
case CanvasCursorVisibilityMode::not_painting:
plan.visible = !input.pen_is_drawing;
plan.uses_pen_state = true;
break;
}
if (input.alt_down || input.pen_is_resizing || input.pen_is_picking) {
plan.visible = true;
plan.forced_visible_by_modifier_or_tool = true;
}
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
const CanvasToolPlan& plan,
CanvasToolServices& services)
{
switch (plan.operation) {
case CanvasToolOperation::select_mode:
if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) {
return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode");
}
if (plan.transform_action != transform_action_for_mode(plan.mode)) {
return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action");
}
services.select_toolbar_button(plan.mode);
if (plan.transform_action != CanvasToolTransformAction::none) {
services.set_transform_action(plan.transform_action);
}
services.set_canvas_mode(plan.mode);
return pp::foundation::Status::success();
case CanvasToolOperation::toggle_picking:
if (!plan.requires_draw_mode) {
return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode");
}
if (plan.no_op) {
return pp::foundation::Status::success();
}
if (!plan.toggles_picking) {
return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op");
}
services.toggle_picking();
return pp::foundation::Status::success();
case CanvasToolOperation::toggle_touch_lock:
if (!plan.toggles_touch_lock || plan.no_op) {
return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock");
}
services.toggle_touch_lock();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown canvas tool operation");
}
} // namespace pp::app

115
src/app_core/canvas_view.h Normal file
View 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

View 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

View File

@@ -0,0 +1,836 @@
#pragma once
#include "foundation/result.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace pp::app {
inline constexpr int document_animation_default_frame_duration = 1;
enum class DocumentAnimationOperation {
add_frame,
duplicate_frame,
remove_frame,
adjust_duration,
move_frame,
select_frame,
goto_frame,
goto_next,
goto_previous,
playback_step,
toggle_playback,
set_onion_size,
};
enum class DocumentAnimationPanelAction {
goto_frame,
next_frame,
previous_frame,
playback_step,
toggle_playback,
};
struct DocumentAnimationPanelState {
int total_duration = 1;
int current_frame = 0;
bool playback_active = false;
};
struct DocumentAnimationOperationPlan {
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
int frame_count = 1;
int current_frame = 0;
int selected_frame = 0;
int target_frame = 0;
int frame_duration = document_animation_default_frame_duration;
int duration_delta = 0;
int move_offset = 0;
int onion_size = 1;
int layer_index = 0;
std::uint32_t layer_id = 0;
int playback_idle_ms = 100;
bool requires_selected_frame = false;
bool mutates_document = false;
bool reloads_animation_layers = false;
bool updates_canvas_animation = false;
bool marks_unsaved = false;
bool playback_was_active = false;
bool playback_active = false;
bool resets_playback_timer = false;
};
struct DocumentAnimationOnionFrameRange {
int frame_count = 1;
int current_frame = 0;
int onion_size = 0;
int first_frame = 0;
int last_frame = 0;
};
inline constexpr float document_animation_timeline_frame_width = 35.0F;
struct DocumentAnimationTimelineScrubPlan {
int total_duration = 1;
float cursor_x = 0.0F;
float frame_width = document_animation_timeline_frame_width;
int target_frame = 0;
};
struct DocumentAnimationLayerInput {
int layer_index = 0;
std::uint32_t layer_id = 0;
std::string name;
bool visible = true;
std::vector<int> frame_durations;
};
struct DocumentAnimationFrameView {
int frame_index = 0;
int duration = document_animation_default_frame_duration;
bool selected = false;
};
struct DocumentAnimationLayerView {
int layer_index = 0;
std::uint32_t layer_id = 0;
std::string name;
bool visible = true;
bool current = false;
std::vector<DocumentAnimationFrameView> frames;
};
struct DocumentAnimationPanelView {
int total_duration = 1;
int current_frame = 0;
int onion_size = 0;
std::uint32_t selected_layer_id = 0;
int selected_frame = -1;
bool has_selected_frame = false;
std::vector<DocumentAnimationLayerView> layers;
};
class DocumentAnimationServices {
public:
virtual ~DocumentAnimationServices() = default;
virtual void add_frame() = 0;
virtual void duplicate_frame(int selected_frame) = 0;
virtual void remove_frame(int selected_frame, int target_frame) = 0;
virtual void set_frame_duration(int selected_frame, int duration) = 0;
virtual int move_frame(int selected_frame, int move_offset) = 0;
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
virtual void select_layer(int layer_index) = 0;
virtual void goto_frame(int target_frame) = 0;
virtual void set_timeline_frame(int target_frame) = 0;
virtual void set_onion_size(int onion_size) = 0;
virtual void capture_playback_restore_mode() = 0;
virtual void enter_playback_camera_mode() = 0;
virtual void restore_playback_canvas_mode() = 0;
virtual void set_playback_active(bool active) = 0;
virtual void reset_playback_timer() = 0;
virtual void set_playback_idle_ms(int idle_ms) = 0;
virtual void update_canvas_animation() = 0;
virtual void update_frame_status() = 0;
virtual void reload_animation_layers() = 0;
virtual void mark_unsaved() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
{
if (frame_count <= 0) {
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
int frame_count,
int index) noexcept
{
const auto count_status = validate_animation_frame_count(frame_count);
if (!count_status.ok()) {
return count_status;
}
if (index < 0 || index >= frame_count) {
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
{
if (duration < 1) {
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationPanelView> plan_animation_panel_view(
const std::vector<DocumentAnimationLayerInput>& layers,
int total_duration,
int current_layer_index,
int current_frame,
std::uint32_t selected_layer_id,
int selected_frame,
int onion_size)
{
if (layers.empty()) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
pp::foundation::Status::invalid_argument("animation panel requires at least one layer"));
}
const auto timeline_status = validate_animation_frame_index(total_duration, current_frame);
if (!timeline_status.ok()) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(timeline_status);
}
if (current_layer_index < 0 || current_layer_index >= static_cast<int>(layers.size())) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
pp::foundation::Status::out_of_range("current animation layer index is outside the document"));
}
if (onion_size < 0) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
}
DocumentAnimationPanelView view;
view.total_duration = total_duration;
view.current_frame = current_frame;
view.onion_size = onion_size;
view.selected_layer_id = selected_layer_id;
view.selected_frame = selected_frame;
view.layers.reserve(layers.size());
for (std::size_t i = 0; i < layers.size(); ++i) {
const auto& input = layers[i];
if (input.layer_index < 0) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
}
if (input.frame_durations.empty()) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
pp::foundation::Status::invalid_argument("animation layer must contain at least one frame"));
}
DocumentAnimationLayerView layer;
layer.layer_index = input.layer_index;
layer.layer_id = input.layer_id;
layer.name = input.name;
layer.visible = input.visible;
layer.current = input.layer_index == current_layer_index;
layer.frames.reserve(input.frame_durations.size());
for (std::size_t frame_index = 0; frame_index < input.frame_durations.size(); ++frame_index) {
const int duration = input.frame_durations[frame_index];
const auto duration_status = validate_animation_frame_duration(duration);
if (!duration_status.ok()) {
return pp::foundation::Result<DocumentAnimationPanelView>::failure(duration_status);
}
const bool selected = selected_frame >= 0
&& input.layer_id == selected_layer_id
&& static_cast<int>(frame_index) == selected_frame;
view.has_selected_frame = view.has_selected_frame || selected;
layer.frames.push_back(DocumentAnimationFrameView {
.frame_index = static_cast<int>(frame_index),
.duration = duration,
.selected = selected,
});
}
view.layers.push_back(std::move(layer));
}
return pp::foundation::Result<DocumentAnimationPanelView>::success(std::move(view));
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
int frame_count,
int current_frame,
int onion_size)
{
const auto index_status = validate_animation_frame_index(frame_count, current_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(index_status);
}
if (onion_size < 0) {
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
}
const auto first = std::max<std::int64_t>(
static_cast<std::int64_t>(current_frame) - static_cast<std::int64_t>(onion_size),
0);
const auto last = std::min<std::int64_t>(
static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(onion_size),
static_cast<std::int64_t>(frame_count) - 1);
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::success(
DocumentAnimationOnionFrameRange {
.frame_count = frame_count,
.current_frame = current_frame,
.onion_size = onion_size,
.first_frame = static_cast<int>(first),
.last_frame = static_cast<int>(last),
});
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationTimelineScrubPlan> plan_animation_timeline_scrub(
int total_duration,
float cursor_x,
float frame_width = document_animation_timeline_frame_width)
{
if (total_duration <= 0) {
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
pp::foundation::Status::invalid_argument("animation timeline duration must be greater than zero"));
}
if (!std::isfinite(cursor_x)) {
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
pp::foundation::Status::invalid_argument("animation timeline cursor position must be finite"));
}
if (!std::isfinite(frame_width) || frame_width <= 0.0F) {
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
pp::foundation::Status::invalid_argument("animation timeline frame width must be positive and finite"));
}
const auto raw_frame = static_cast<std::int64_t>(std::floor(cursor_x / frame_width));
const auto target_frame = std::clamp<std::int64_t>(raw_frame, 0, total_duration - 1);
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::success(
DocumentAnimationTimelineScrubPlan {
.total_duration = total_duration,
.cursor_x = cursor_x,
.frame_width = frame_width,
.target_frame = static_cast<int>(target_frame),
});
}
[[nodiscard]] inline float animation_onion_frame_alpha(
const DocumentAnimationOnionFrameRange& range,
int frame) noexcept
{
if (frame < range.first_frame || frame > range.last_frame) {
return 0.0f;
}
const int distance = frame >= range.current_frame
? frame - range.current_frame
: range.current_frame - frame;
if (distance > range.onion_size) {
return 0.0f;
}
return 1.0f - static_cast<float>(distance) / static_cast<float>(range.onion_size + 1);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
int frame_count,
int current_frame)
{
const auto count_status = validate_animation_frame_count(frame_count);
if (!count_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
}
if (current_frame < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::add_frame;
plan.frame_count = frame_count;
plan.current_frame = current_frame;
plan.selected_frame = frame_count;
plan.target_frame = current_frame;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
int frame_count,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::duplicate_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame + 1;
plan.requires_selected_frame = true;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
int frame_count,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (frame_count <= 1) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::remove_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = std::min(selected_frame, frame_count - 2);
plan.requires_selected_frame = true;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
int frame_count,
int selected_frame,
int current_duration,
int delta)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
const auto duration_status = validate_animation_frame_duration(current_duration);
if (!duration_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
}
if (delta == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
}
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::adjust_duration;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame;
plan.frame_duration = std::max(current_duration + delta, 1);
plan.duration_delta = delta;
plan.requires_selected_frame = true;
plan.mutates_document = plan.frame_duration != current_duration;
plan.reloads_animation_layers = plan.mutates_document;
plan.updates_canvas_animation = plan.mutates_document;
plan.marks_unsaved = plan.mutates_document;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
int frame_count,
int selected_frame,
int offset)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (offset == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::move_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
plan.move_offset = offset;
plan.requires_selected_frame = true;
plan.mutates_document = plan.target_frame != selected_frame;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = plan.mutates_document;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
int frame_count,
int layer_index,
std::uint32_t layer_id,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (layer_index < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::select_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame;
plan.layer_index = layer_index;
plan.layer_id = layer_id;
plan.requires_selected_frame = true;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
int total_duration,
int frame)
{
if (total_duration <= 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
}
if (frame < 0 || frame >= total_duration) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::goto_frame;
plan.frame_count = total_duration;
plan.current_frame = frame;
plan.target_frame = frame;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
int total_duration,
int current_frame,
int offset)
{
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
if (!current_status) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
}
if (offset == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
}
DocumentAnimationOperationPlan plan;
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
plan.frame_count = total_duration;
plan.current_frame = current_frame;
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
% static_cast<std::int64_t>(total_duration);
if (target < 0) {
target += total_duration;
}
plan.target_frame = static_cast<int>(target);
plan.updates_canvas_animation = true;
plan.reloads_animation_layers = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
int total_duration,
int current_frame,
int offset)
{
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
if (!step) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
}
auto plan = step.value();
plan.operation = DocumentAnimationOperation::playback_step;
plan.move_offset = offset;
plan.reloads_animation_layers = false;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
bool playback_active)
{
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::toggle_playback;
plan.playback_was_active = playback_active;
plan.playback_active = !playback_active;
plan.playback_idle_ms = playback_active ? 100 : 10;
plan.resets_playback_timer = !playback_active;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
{
if (onion_size < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::set_onion_size;
plan.onion_size = onion_size;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
DocumentAnimationPanelAction action,
const DocumentAnimationPanelState& state,
int target_frame = 0)
{
switch (action) {
case DocumentAnimationPanelAction::goto_frame:
return plan_animation_goto_frame(state.total_duration, target_frame);
case DocumentAnimationPanelAction::next_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::previous_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
case DocumentAnimationPanelAction::playback_step:
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::toggle_playback:
return plan_animation_playback_toggle(state.playback_active);
}
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("unknown animation panel action"));
}
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
const DocumentAnimationOperationPlan& plan) noexcept
{
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
if (!plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
}
return validate_animation_frame_count(plan.frame_count);
case DocumentAnimationOperation::duplicate_frame:
case DocumentAnimationOperation::remove_frame:
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
}
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::adjust_duration:
if (!plan.requires_selected_frame) {
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_duration(plan.frame_duration);
case DocumentAnimationOperation::move_frame:
if (!plan.requires_selected_frame || plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::select_frame:
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
case DocumentAnimationOperation::playback_step:
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
}
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active == plan.playback_was_active) {
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
}
if (plan.playback_idle_ms <= 0) {
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
}
if (plan.playback_active && !plan.resets_playback_timer) {
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
if (plan.onion_size < 0) {
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
}
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
const DocumentAnimationOperationPlan& plan,
DocumentAnimationServices& services)
{
const auto validation = validate_animation_operation_plan(plan);
if (!validation.ok()) {
return validation;
}
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
services.add_frame();
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::duplicate_frame:
services.duplicate_frame(plan.selected_frame);
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::remove_frame:
services.remove_frame(plan.selected_frame, plan.target_frame);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::adjust_duration:
if (plan.mutates_document) {
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::move_frame:
{
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
if (plan.marks_unsaved) {
services.mark_unsaved();
}
if (plan.updates_canvas_animation) {
services.goto_frame(actual_target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
}
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
services.goto_frame(plan.target_frame);
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::select_frame:
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
services.select_layer(plan.layer_index);
return pp::foundation::Status::success();
case DocumentAnimationOperation::playback_step:
services.goto_frame(plan.target_frame);
services.set_timeline_frame(plan.target_frame);
services.update_frame_status();
return pp::foundation::Status::success();
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active) {
services.capture_playback_restore_mode();
services.enter_playback_camera_mode();
if (plan.resets_playback_timer) {
services.reset_playback_timer();
}
} else {
services.restore_playback_canvas_mode();
}
services.set_playback_active(plan.playback_active);
services.set_playback_idle_ms(plan.playback_idle_ms);
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
services.set_onion_size(plan.onion_size);
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
} // namespace pp::app

View File

@@ -0,0 +1,526 @@
#pragma once
#include "document/document.h"
#include "document/ppi_export.h"
#include "foundation/result.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace pp::app {
struct DocumentCanvasClearPlan {
float r = 0.0F;
float g = 0.0F;
float b = 0.0F;
float a = 0.0F;
bool clears_canvas = false;
bool records_undo = false;
bool marks_unsaved = false;
bool no_op = true;
};
struct DocumentCanvasFacePayloadInput {
std::uint32_t frame_index = 0;
std::uint32_t face_index = 0;
std::uint32_t x = 0;
std::uint32_t y = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::span<const std::uint8_t> rgba8;
};
struct DocumentCanvasLayerSnapshotInput {
std::string_view name;
bool visible = true;
bool alpha_locked = false;
float opacity = 1.0F;
int blend_mode = 0;
std::span<const std::uint32_t> frame_durations_ms;
std::size_t pending_face_payloads = 0;
std::span<const DocumentCanvasFacePayloadInput> captured_face_payloads;
};
struct DocumentCanvasSnapshotInput {
bool has_canvas = true;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::size_t active_layer_index = 0;
std::size_t active_frame_index = 0;
std::span<const DocumentCanvasLayerSnapshotInput> layers;
};
struct DocumentCanvasSnapshotResult {
pp::document::CanvasDocument document;
std::size_t layer_count = 0;
std::size_t frame_count = 0;
std::size_t pending_face_payloads = 0;
std::size_t captured_face_payloads = 0;
bool metadata_only = false;
bool requires_renderer_payload_readback = false;
};
struct DocumentCanvasSaveSnapshotReport {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::size_t layer_count = 0;
std::size_t frame_count = 0;
std::size_t captured_face_payloads = 0;
std::size_t pending_face_payloads = 0;
bool payload_complete = false;
bool can_export_ppi = false;
};
enum class DocumentCanvasSaveWriterAction {
use_document_ppi_writer,
use_legacy_project_save,
};
struct DocumentCanvasSaveWriterRoutePlan {
DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save;
bool payload_complete = false;
bool can_export_ppi = false;
bool uses_document_ppi_writer = false;
std::string_view fallback_reason;
};
struct DocumentCanvasPpiExportResult {
DocumentCanvasSaveSnapshotReport report;
std::vector<std::byte> bytes;
};
struct DocumentCanvasProjectSaveTargetPlan {
std::string target_path;
std::string file_name;
std::string temporary_path;
std::string timelapse_path;
};
enum class DocumentCanvasProjectSaveWriteAction {
write_direct_to_target,
write_temporary_then_swap,
};
struct DocumentCanvasProjectSaveWritePlan {
DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target;
std::string write_path;
std::string target_path;
std::string temporary_path;
bool target_exists = false;
bool uses_temporary = false;
bool falls_back_to_direct_on_temporary_open_failure = false;
};
struct DocumentCanvasProjectSaveCommitInput {
bool used_temporary = false;
bool target_remove_attempted = false;
bool target_remove_succeeded = false;
bool temporary_rename_attempted = false;
bool temporary_rename_succeeded = false;
};
struct DocumentCanvasProjectSaveCommitPlan {
bool saved = false;
bool used_temporary = false;
bool target_removed = false;
bool temporary_renamed = false;
bool target_may_be_missing = false;
std::string_view log_message;
};
struct DocumentCanvasProjectSavePostCommitInput {
bool save_succeeded = false;
bool timelapse_encoder_available = false;
bool progress_ui_visible = false;
};
struct DocumentCanvasProjectSavePostCommitPlan {
bool marks_document_clean = false;
bool marks_new_document_committed = false;
bool saves_timelapse_sidecar = false;
bool flushes_platform_storage = false;
bool dismisses_progress_ui = false;
bool updates_title = true;
};
class DocumentCanvasClearServices {
public:
virtual ~DocumentCanvasClearServices() = default;
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
{
if (!std::isfinite(value)) {
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
}
if (value < 0.0F || value > 1.0F) {
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
DocumentCanvasSnapshotInput input)
{
if (!input.has_canvas) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
}
if (input.layers.empty()) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
}
std::size_t frame_count = 1U;
std::size_t pending_face_payloads = 0U;
std::size_t captured_face_payloads = 0U;
for (const auto& layer : input.layers) {
frame_count = std::max(frame_count, layer.frame_durations_ms.size());
pending_face_payloads += layer.pending_face_payloads;
captured_face_payloads += layer.captured_face_payloads.size();
}
if (input.active_layer_index >= input.layers.size()) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
}
if (input.active_frame_index >= frame_count) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
}
std::vector<pp::document::AnimationFrame> root_frames;
root_frames.reserve(frame_count);
for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
std::uint32_t duration_ms = 100U;
for (const auto& layer : input.layers) {
if (frame_index < layer.frame_durations_ms.size()) {
duration_ms = layer.frame_durations_ms[frame_index];
break;
}
}
root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
}
std::vector<std::string> layer_names;
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
std::vector<pp::document::DocumentLayerConfig> layer_configs;
layer_names.reserve(input.layers.size());
layer_frames.reserve(input.layers.size());
layer_configs.reserve(input.layers.size());
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
const auto& layer = input.layers[layer_index];
if (layer.name.empty()) {
layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
} else {
layer_names.push_back(std::string(layer.name));
}
layer_frames.push_back({});
auto& frames = layer_frames.back();
frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
if (layer.frame_durations_ms.empty()) {
frames = root_frames;
} else {
for (const auto duration_ms : layer.frame_durations_ms) {
frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
}
}
layer_configs.push_back(pp::document::DocumentLayerConfig {
.name = layer_names.back(),
.visible = layer.visible,
.alpha_locked = layer.alpha_locked,
.opacity = layer.opacity,
.blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
.frames = std::span<const pp::document::AnimationFrame>(frames),
});
}
auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
.width = input.width,
.height = input.height,
.layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
.frames = std::span<const pp::document::AnimationFrame>(root_frames),
});
if (!document) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
}
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
for (const auto& payload : input.layers[layer_index].captured_face_payloads) {
pp::document::LayerFacePixels pixels {
.face_index = payload.face_index,
.x = payload.x,
.y = payload.y,
.width = payload.width,
.height = payload.height,
.rgba8 = std::vector<std::uint8_t>(payload.rgba8.begin(), payload.rgba8.end()),
};
const auto payload_status = document.value().set_layer_frame_face_pixels(
layer_index,
payload.frame_index,
std::move(pixels));
if (!payload_status.ok()) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(payload_status);
}
}
}
auto active_status = document.value().set_active_layer(input.active_layer_index);
if (!active_status.ok()) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
}
active_status = document.value().set_active_frame(input.active_frame_index);
if (!active_status.ok()) {
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
}
return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
.document = std::move(document.value()),
.layer_count = input.layers.size(),
.frame_count = frame_count,
.pending_face_payloads = pending_face_payloads,
.captured_face_payloads = captured_face_payloads,
.metadata_only = captured_face_payloads == 0U,
.requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads,
});
}
[[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report(
const DocumentCanvasSnapshotResult& snapshot) noexcept
{
return DocumentCanvasSaveSnapshotReport {
.width = snapshot.document.width(),
.height = snapshot.document.height(),
.layer_count = snapshot.layer_count,
.frame_count = snapshot.frame_count,
.captured_face_payloads = snapshot.captured_face_payloads,
.pending_face_payloads = snapshot.pending_face_payloads,
.payload_complete = !snapshot.requires_renderer_payload_readback,
.can_export_ppi = !snapshot.requires_renderer_payload_readback,
};
}
[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route(
DocumentCanvasSaveSnapshotReport report) noexcept
{
DocumentCanvasSaveWriterRoutePlan plan;
plan.payload_complete = report.payload_complete;
plan.can_export_ppi = report.can_export_ppi;
if (!report.payload_complete || !report.can_export_ppi) {
plan.fallback_reason = "canvas document snapshot still requires renderer payload readback";
return plan;
}
plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer;
plan.uses_document_ppi_writer = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasPpiExportResult>
export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot)
{
const auto report = make_document_canvas_save_snapshot_report(snapshot);
const auto route = plan_document_canvas_save_writer_route(report);
if (!route.uses_document_ppi_writer) {
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(
pp::foundation::Status::invalid_argument(route.fallback_reason.data()));
}
auto bytes = pp::document::export_ppi_project_document(snapshot.document);
if (!bytes) {
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(bytes.status());
}
return pp::foundation::Result<DocumentCanvasPpiExportResult>::success(DocumentCanvasPpiExportResult {
.report = report,
.bytes = std::move(bytes.value()),
});
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>
plan_document_canvas_project_save_target(
std::string_view data_directory,
std::string_view target_path)
{
if (data_directory.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
pp::foundation::Status::invalid_argument("project save data directory must not be empty"));
}
if (target_path.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
pp::foundation::Status::invalid_argument("project save target path must not be empty"));
}
const auto basename_start = target_path.find_last_of("/\\");
const auto file_name_start = basename_start == std::string_view::npos ? 0U : basename_start + 1U;
auto file_name = target_path.substr(file_name_start);
if (file_name.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
pp::foundation::Status::invalid_argument("project save target file name must not be empty"));
}
constexpr std::string_view ppi_extension = ".ppi";
if (file_name.size() > ppi_extension.size()
&& file_name.substr(file_name.size() - ppi_extension.size()) == ppi_extension) {
file_name.remove_suffix(ppi_extension.size());
}
DocumentCanvasProjectSaveTargetPlan plan;
plan.target_path = std::string(target_path);
plan.file_name = std::string(file_name);
plan.temporary_path.reserve(data_directory.size() + plan.file_name.size() + 10U);
plan.temporary_path += data_directory;
plan.temporary_path += "/";
plan.temporary_path += plan.file_name;
plan.temporary_path += ".tmp.ppi";
plan.timelapse_path.reserve(data_directory.size() + plan.file_name.size() + 6U);
plan.timelapse_path += data_directory;
plan.timelapse_path += "/";
plan.timelapse_path += plan.file_name;
plan.timelapse_path += ".pptl";
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>
plan_document_canvas_project_save_write(
const DocumentCanvasProjectSaveTargetPlan& target,
bool target_exists)
{
if (target.target_path.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
pp::foundation::Status::invalid_argument("project save write target path must not be empty"));
}
DocumentCanvasProjectSaveWritePlan plan;
plan.target_exists = target_exists;
plan.target_path = target.target_path;
plan.temporary_path = target.temporary_path;
if (!target_exists) {
plan.write_path = target.target_path;
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
}
if (target.temporary_path.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
pp::foundation::Status::invalid_argument("project save temporary path must not be empty"));
}
plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap;
plan.write_path = target.temporary_path;
plan.uses_temporary = true;
plan.falls_back_to_direct_on_temporary_open_failure = true;
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
}
[[nodiscard]] constexpr DocumentCanvasProjectSaveCommitPlan plan_document_canvas_project_save_commit(
DocumentCanvasProjectSaveCommitInput input) noexcept
{
DocumentCanvasProjectSaveCommitPlan plan;
plan.used_temporary = input.used_temporary;
if (!input.used_temporary) {
plan.saved = true;
plan.log_message = "project saved to target";
return plan;
}
if (!input.target_remove_attempted || !input.target_remove_succeeded) {
plan.log_message = "could not remove target project before temporary swap";
return plan;
}
plan.target_removed = true;
if (!input.temporary_rename_attempted || !input.temporary_rename_succeeded) {
plan.target_may_be_missing = true;
plan.log_message = "temporary project not swapped after original removal";
return plan;
}
plan.saved = true;
plan.temporary_renamed = true;
plan.log_message = "temporary project swapped successfully";
return plan;
}
[[nodiscard]] constexpr DocumentCanvasProjectSavePostCommitPlan plan_document_canvas_project_save_post_commit(
DocumentCanvasProjectSavePostCommitInput input) noexcept
{
DocumentCanvasProjectSavePostCommitPlan plan;
plan.dismisses_progress_ui = input.progress_ui_visible;
if (!input.save_succeeded) {
return plan;
}
plan.marks_document_clean = true;
plan.marks_new_document_committed = true;
plan.saves_timelapse_sidecar = input.timelapse_encoder_available;
plan.flushes_platform_storage = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
bool has_canvas,
float r = 0.0F,
float g = 0.0F,
float b = 0.0F,
float a = 0.0F) noexcept
{
const float channels[] { r, g, b, a };
for (const float channel : channels) {
const auto status = validate_clear_color_channel(channel);
if (!status.ok()) {
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
}
}
DocumentCanvasClearPlan plan;
plan.r = r;
plan.g = g;
plan.b = b;
plan.a = a;
plan.clears_canvas = has_canvas;
plan.records_undo = has_canvas;
plan.marks_unsaved = has_canvas;
plan.no_op = !has_canvas;
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
const DocumentCanvasClearPlan& plan,
DocumentCanvasClearServices& services)
{
const float channels[] { plan.r, plan.g, plan.b, plan.a };
for (const float channel : channels) {
const auto status = validate_clear_color_channel(channel);
if (!status.ok()) {
return status;
}
}
if (plan.no_op || !plan.clears_canvas) {
return pp::foundation::Status::success();
}
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
return pp::foundation::Status::success();
}
} // namespace pp::app

View File

@@ -0,0 +1,315 @@
#pragma once
#include "app_core/app_dialog.h"
#include "foundation/result.h"
#include <cstdio>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
namespace pp::app {
enum class CloudUploadAction {
unavailable_no_canvas,
show_save_required_warning,
prompt_publish,
};
enum class CloudBrowseAction {
unavailable_no_canvas,
show_browser,
};
enum class CloudDownloadSelectionAction {
wait_for_selection,
start_download,
};
enum class CloudTransferDirection {
download,
upload,
};
enum class CloudTransferAction {
reject_missing_source,
reject_missing_destination,
start_transfer,
};
struct CloudUploadPlan {
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
bool save_before_upload = false;
};
struct CloudBulkUploadPlan {
std::size_t file_count = 0;
int progress_total = 0;
bool show_progress = false;
};
struct CloudDownloadRequest {
std::string selected_file;
std::string selected_path;
std::string selected_name;
};
struct CloudTransferPlan {
CloudTransferDirection direction = CloudTransferDirection::download;
CloudTransferAction action = CloudTransferAction::reject_missing_source;
bool enable_progress = false;
bool disable_tls_verification = false;
};
struct CloudTransferProgressPlan {
bool notify = false;
float fraction = 0.0F;
};
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_save_required_prompt()
{
return plan_app_message_dialog(
"Warning",
"This document needs to be saved before upload.",
false);
}
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_publish_prompt()
{
return plan_app_message_dialog(
"Publish document",
"Would you like to upload to the public domain?",
true,
"Yes",
"No");
}
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_upload_success_prompt()
{
return plan_app_message_dialog(
"Success",
"This document has been succesfully uploaded.",
false);
}
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_upload_progress_dialog()
{
return plan_app_progress_dialog("Uploading", 0);
}
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_bulk_upload_progress_dialog(int progress_total)
{
return plan_app_progress_dialog("Export Pano Image", progress_total);
}
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_download_progress_prompt()
{
return plan_app_message_dialog(
"Downloading",
"Download in progress",
true);
}
[[nodiscard]] inline std::string format_cloud_download_progress_message(float progress_fraction)
{
char buffer[64] {};
std::snprintf(
buffer,
sizeof(buffer),
"Download in progress %.2f%%",
progress_fraction * 100.0F);
return buffer;
}
class CloudServices {
public:
virtual ~CloudServices() = default;
virtual void show_save_required_warning() = 0;
virtual void prompt_publish(bool save_before_upload) = 0;
virtual void begin_bulk_upload(int progress_total, bool show_progress) = 0;
virtual void upload_all_bulk_files() = 0;
virtual void end_bulk_upload() = 0;
virtual void show_browser() = 0;
virtual void start_download(const CloudDownloadRequest& request) = 0;
};
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
bool has_canvas,
bool is_new_document,
bool has_unsaved_changes) noexcept
{
if (!has_canvas) {
return { CloudUploadAction::unavailable_no_canvas, false };
}
if (is_new_document) {
return { CloudUploadAction::show_save_required_warning, false };
}
return { CloudUploadAction::prompt_publish, has_unsaved_changes };
}
[[nodiscard]] constexpr CloudBrowseAction plan_cloud_browse(bool has_canvas) noexcept
{
return has_canvas
? CloudBrowseAction::show_browser
: CloudBrowseAction::unavailable_no_canvas;
}
[[nodiscard]] constexpr CloudDownloadSelectionAction plan_cloud_download_selection(
std::string_view selected_file) noexcept
{
return selected_file.empty()
? CloudDownloadSelectionAction::wait_for_selection
: CloudDownloadSelectionAction::start_download;
}
[[nodiscard]] constexpr CloudBulkUploadPlan plan_cloud_bulk_upload(
std::size_t file_count,
bool progress_ui_available) noexcept
{
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
return {
file_count,
file_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(file_count),
progress_ui_available,
};
}
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_download_transfer(
std::string_view url,
std::string_view destination_path,
bool has_progress_callback,
bool disables_tls_verification) noexcept
{
if (url.empty()) {
return {
CloudTransferDirection::download,
CloudTransferAction::reject_missing_source,
false,
false,
};
}
if (destination_path.empty()) {
return {
CloudTransferDirection::download,
CloudTransferAction::reject_missing_destination,
false,
false,
};
}
return {
CloudTransferDirection::download,
CloudTransferAction::start_transfer,
has_progress_callback,
disables_tls_verification,
};
}
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_upload_transfer(
std::string_view filename,
bool has_progress_callback,
bool disables_tls_verification) noexcept
{
if (filename.empty()) {
return {
CloudTransferDirection::upload,
CloudTransferAction::reject_missing_source,
false,
false,
};
}
return {
CloudTransferDirection::upload,
CloudTransferAction::start_transfer,
has_progress_callback,
disables_tls_verification,
};
}
[[nodiscard]] constexpr CloudTransferProgressPlan plan_cloud_transfer_progress(
std::int64_t total,
std::int64_t current) noexcept
{
if (total <= 0) {
return {};
}
const auto clamped_current = current < 0
? std::int64_t { 0 }
: (current > total ? total : current);
return {
true,
static_cast<float>(static_cast<double>(clamped_current) / static_cast<double>(total)),
};
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
const CloudUploadPlan& plan,
CloudServices& services)
{
switch (plan.action) {
case CloudUploadAction::unavailable_no_canvas:
return pp::foundation::Status::success();
case CloudUploadAction::show_save_required_warning:
services.show_save_required_warning();
return pp::foundation::Status::success();
case CloudUploadAction::prompt_publish:
services.prompt_publish(plan.save_before_upload);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud upload action");
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_bulk_upload_plan(
const CloudBulkUploadPlan& plan,
CloudServices& services)
{
services.begin_bulk_upload(plan.progress_total, plan.show_progress);
services.upload_all_bulk_files();
services.end_bulk_upload();
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_browse_action(
CloudBrowseAction action,
CloudServices& services)
{
switch (action) {
case CloudBrowseAction::unavailable_no_canvas:
return pp::foundation::Status::success();
case CloudBrowseAction::show_browser:
services.show_browser();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud browse action");
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_download_selection_action(
CloudDownloadSelectionAction action,
CloudServices& services,
const CloudDownloadRequest& request)
{
switch (action) {
case CloudDownloadSelectionAction::wait_for_selection:
return pp::foundation::Status::success();
case CloudDownloadSelectionAction::start_download:
if (request.selected_file.empty()) {
return pp::foundation::Status::invalid_argument("cloud download requires a selected file");
}
services.start_download(request);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown cloud download selection action");
}
}

View File

@@ -0,0 +1 @@
#include "app_core/document_export.h"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
#pragma once
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class DocumentImageImportAction {
import_equirectangular,
place_transform,
};
struct DocumentImageImportPlan {
int width = 0;
int height = 0;
DocumentImageImportAction action = DocumentImageImportAction::place_transform;
bool imports_equirectangular = false;
bool enters_transform_mode = false;
};
class DocumentImageImportServices {
public:
virtual ~DocumentImageImportServices() = default;
virtual void import_equirectangular(std::string_view path) = 0;
virtual void enter_transform_import(std::string_view path) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_document_image_import_dimensions(
int width,
int height) noexcept
{
if (width <= 0 || height <= 0) {
return pp::foundation::Status::invalid_argument("image dimensions must be positive");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentImageImportPlan> plan_document_image_import(
int width,
int height) noexcept
{
const auto dimensions = validate_document_image_import_dimensions(width, height);
if (!dimensions.ok()) {
return pp::foundation::Result<DocumentImageImportPlan>::failure(dimensions);
}
const auto wide_equirect = static_cast<long long>(width) == static_cast<long long>(height) * 2LL;
const auto vertical_cube_strip = width == height / 6;
DocumentImageImportPlan plan;
plan.width = width;
plan.height = height;
plan.imports_equirectangular = wide_equirect || vertical_cube_strip;
plan.enters_transform_mode = !plan.imports_equirectangular;
plan.action = plan.imports_equirectangular
? DocumentImageImportAction::import_equirectangular
: DocumentImageImportAction::place_transform;
return pp::foundation::Result<DocumentImageImportPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_document_image_import_plan(
const DocumentImageImportPlan& plan,
std::string_view path,
DocumentImageImportServices& services)
{
const auto dimensions = validate_document_image_import_dimensions(plan.width, plan.height);
if (!dimensions.ok()) {
return dimensions;
}
if (path.empty()) {
return pp::foundation::Status::invalid_argument("image import path must not be empty");
}
switch (plan.action) {
case DocumentImageImportAction::import_equirectangular:
services.import_equirectangular(path);
return pp::foundation::Status::success();
case DocumentImageImportAction::place_transform:
services.enter_transform_import(path);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown image import action");
}
} // namespace pp::app

View File

@@ -0,0 +1,720 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace pp::app {
inline constexpr std::size_t document_layer_name_max_length = 128;
inline constexpr int document_layer_legacy_blend_mode_count = 5;
enum class DocumentLayerRenameAction {
no_op_same_name,
rename_and_record_undo,
};
enum class DocumentLayerOperation {
add,
duplicate,
select,
reorder,
remove,
set_opacity,
set_visibility,
set_alpha_lock,
set_blend_mode,
set_highlight,
};
enum class DocumentLayerMenuCommand {
clear,
rename,
merge_down,
};
enum class DocumentLayerMenuAction {
clear_current_layer,
show_rename_dialog,
merge_with_lower_layer,
show_merge_animated_not_supported,
no_op_select_layer,
no_op_select_upper_layer,
};
struct DocumentLayerRenamePlan {
std::string old_name;
std::string new_name;
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
};
struct DocumentLayerOperationPlan {
DocumentLayerOperation operation = DocumentLayerOperation::select;
int index = 0;
int from_index = 0;
int to_index = 0;
int insert_index = 0;
int source_index = 0;
std::string name;
float opacity = 1.0F;
bool flag = false;
int blend_mode = 0;
bool mutates_document = false;
bool marks_unsaved = false;
bool reloads_animation_layers = false;
bool updates_title = false;
};
struct DocumentLayerMenuPlan {
DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
std::string label;
int from_index = 0;
int to_index = 0;
};
struct DocumentLayerMergePlan {
int from_index = 0;
int to_index = 0;
bool create_history = true;
};
struct DocumentLayerPanelInput {
int layer_index = 0;
std::string name;
float opacity = 1.0F;
bool visible = true;
bool alpha_locked = false;
int blend_mode = 0;
};
struct DocumentLayerPanelLayerView {
int layer_index = 0;
std::string name;
float opacity = 1.0F;
bool visible = true;
bool alpha_locked = false;
int blend_mode = 0;
bool current = false;
};
struct DocumentLayerPanelView {
int current_index = 0;
float current_opacity = 1.0F;
bool current_alpha_locked = false;
int current_blend_mode = 0;
std::vector<DocumentLayerPanelLayerView> layers;
};
class DocumentLayerMenuServices {
public:
virtual ~DocumentLayerMenuServices() = default;
virtual void clear_current_layer() = 0;
virtual void show_rename_dialog() = 0;
virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
virtual void show_merge_animated_not_supported() = 0;
};
class DocumentLayerRenameServices {
public:
virtual ~DocumentLayerRenameServices() = default;
virtual void rename_layer(std::string_view old_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 pp::foundation::Status validate_layer_index(
int layer_count,
int index) noexcept
{
if (layer_count <= 0) {
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
}
if (index < 0 || index >= layer_count) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
int layer_count,
int index) noexcept
{
if (layer_count < 0) {
return pp::foundation::Status::invalid_argument("layer count must not be negative");
}
if (index < 0 || index > layer_count) {
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerPanelView> plan_document_layer_panel_view(
const std::vector<DocumentLayerPanelInput>& layers,
int current_index)
{
if (layers.empty()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::invalid_argument("layer panel requires at least one layer"));
}
const auto current_status = validate_layer_index(static_cast<int>(layers.size()), current_index);
if (!current_status.ok()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(current_status);
}
DocumentLayerPanelView view;
view.current_index = current_index;
view.layers.reserve(layers.size());
for (const auto& input : layers) {
const auto index_status = validate_layer_index(static_cast<int>(layers.size()), input.layer_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(index_status);
}
if (!std::isfinite(input.opacity) || input.opacity < 0.0F || input.opacity > 1.0F) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
}
if (input.blend_mode < 0 || input.blend_mode >= document_layer_legacy_blend_mode_count) {
return pp::foundation::Result<DocumentLayerPanelView>::failure(
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
}
const bool current = input.layer_index == current_index;
DocumentLayerPanelLayerView layer;
layer.layer_index = input.layer_index;
layer.name = input.name;
layer.opacity = input.opacity;
layer.visible = input.visible;
layer.alpha_locked = input.alpha_locked;
layer.blend_mode = input.blend_mode;
layer.current = current;
if (current) {
view.current_opacity = input.opacity;
view.current_alpha_locked = input.alpha_locked;
view.current_blend_mode = input.blend_mode;
}
view.layers.push_back(std::move(layer));
}
return pp::foundation::Result<DocumentLayerPanelView>::success(std::move(view));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
std::string_view old_name,
std::string_view requested_name)
{
if (requested_name.empty()) {
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
pp::foundation::Status::invalid_argument("layer name must not be empty"));
}
if (requested_name.size() > document_layer_name_max_length) {
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
}
DocumentLayerRenamePlan plan;
plan.old_name = std::string(old_name);
plan.new_name = std::string(requested_name);
plan.action = old_name == requested_name
? DocumentLayerRenameAction::no_op_same_name
: DocumentLayerRenameAction::rename_and_record_undo;
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
int layer_count,
int insert_index,
std::string_view name)
{
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
const auto rename = plan_document_layer_rename({}, name);
if (!rename) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::add;
plan.insert_index = insert_index;
plan.name = std::string(name);
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
int layer_count,
int source_index)
{
const auto index_status = validate_layer_index(layer_count, source_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::duplicate;
plan.source_index = source_index;
plan.insert_index = source_index + 1;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
int layer_count,
int index)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::select;
plan.index = index;
plan.reloads_animation_layers = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
int layer_count,
int from_index,
int to_index)
{
auto index_status = validate_layer_index(layer_count, from_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
index_status = validate_layer_index(layer_count, to_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::reorder;
plan.from_index = from_index;
plan.to_index = to_index;
plan.mutates_document = from_index != to_index;
plan.marks_unsaved = plan.mutates_document;
plan.reloads_animation_layers = plan.mutates_document;
plan.updates_title = plan.mutates_document;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
int layer_count,
int index)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (layer_count <= 1) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::remove;
plan.index = index;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
int layer_count,
int index,
float opacity)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_opacity;
plan.index = index;
plan.opacity = opacity;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
int layer_count,
int index,
bool visible)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_visibility;
plan.index = index;
plan.flag = visible;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
int layer_count,
int index,
bool locked)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_alpha_lock;
plan.index = index;
plan.flag = locked;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
int layer_count,
int index,
int blend_mode)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_blend_mode;
plan.index = index;
plan.blend_mode = blend_mode;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
int layer_count,
int index,
bool highlight)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_highlight;
plan.index = index;
plan.flag = highlight;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
DocumentLayerMenuCommand command,
bool has_current_layer,
int current_index,
int animation_duration,
std::string_view current_layer_name,
std::string_view lower_layer_name)
{
if (current_index < 0) {
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
pp::foundation::Status::out_of_range("current layer index must not be negative"));
}
if (animation_duration < 0) {
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
pp::foundation::Status::out_of_range("animation duration must not be negative"));
}
DocumentLayerMenuPlan plan;
plan.command = command;
plan.from_index = current_index;
plan.to_index = current_index > 0 ? current_index - 1 : 0;
switch (command) {
case DocumentLayerMenuCommand::clear:
plan.action = has_current_layer
? DocumentLayerMenuAction::clear_current_layer
: DocumentLayerMenuAction::no_op_select_layer;
plan.label = has_current_layer
? "Clear Layer " + std::string(current_layer_name)
: "Clear Layer (Select a layer)";
break;
case DocumentLayerMenuCommand::rename:
plan.action = has_current_layer
? DocumentLayerMenuAction::show_rename_dialog
: DocumentLayerMenuAction::no_op_select_layer;
plan.label = has_current_layer
? "Rename Layer " + std::string(current_layer_name)
: "Rename Layer (Select a layer)";
break;
case DocumentLayerMenuCommand::merge_down:
if (!has_current_layer) {
plan.action = DocumentLayerMenuAction::no_op_select_layer;
plan.label = "Merge Layer (Select a layer)";
} else if (animation_duration > 1) {
plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
plan.label = "Merge Layer (Animation not supported)";
} else if (current_index <= 0) {
plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
plan.label = "Merge Layer (Select upper layers)";
} else {
plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
plan.label = "Merge with " + std::string(lower_layer_name);
}
break;
}
return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMergePlan> plan_document_layer_merge(
int layer_count,
int from_index,
int to_index,
int animation_duration,
bool create_history = true)
{
auto index_status = validate_layer_index(layer_count, from_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
}
index_status = validate_layer_index(layer_count, to_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
}
if (animation_duration < 0) {
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
pp::foundation::Status::out_of_range("animation duration must not be negative"));
}
if (animation_duration > 1) {
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
pp::foundation::Status::invalid_argument("animated layer merge is not supported"));
}
if (from_index <= to_index) {
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
pp::foundation::Status::invalid_argument("layer merge source must be above the destination"));
}
DocumentLayerMergePlan plan;
plan.from_index = from_index;
plan.to_index = to_index;
plan.create_history = create_history;
return pp::foundation::Result<DocumentLayerMergePlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_document_layer_rename_plan(
const DocumentLayerRenamePlan& plan,
DocumentLayerRenameServices& services)
{
switch (plan.action) {
case DocumentLayerRenameAction::no_op_same_name:
services.finish_layer_rename();
return pp::foundation::Status::success();
case DocumentLayerRenameAction::rename_and_record_undo:
if (plan.new_name.empty()) {
return pp::foundation::Status::invalid_argument("layer rename plan must include a new name");
}
if (plan.old_name == plan.new_name) {
return pp::foundation::Status::invalid_argument("layer rename plan must change the name");
}
services.rename_layer(plan.old_name, plan.new_name);
services.finish_layer_rename();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document layer rename action");
}
inline void execute_document_layer_operation_side_effects(
const DocumentLayerOperationPlan& plan,
DocumentLayerOperationServices& services)
{
if (plan.marks_unsaved)
services.mark_unsaved();
if (plan.reloads_animation_layers)
services.reload_animation_layers();
if (plan.updates_title)
services.update_title();
}
[[nodiscard]] inline pp::foundation::Status execute_document_layer_operation_plan(
const DocumentLayerOperationPlan& plan,
DocumentLayerOperationServices& services)
{
switch (plan.operation) {
case DocumentLayerOperation::add:
if (!plan.mutates_document || plan.name.empty()) {
return pp::foundation::Status::invalid_argument("layer add plan must mutate with a name");
}
services.add_layer(plan.name, plan.insert_index);
break;
case DocumentLayerOperation::duplicate:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer duplicate plan must mutate the document");
}
services.duplicate_layer(plan.source_index, plan.insert_index);
break;
case DocumentLayerOperation::select:
services.select_layer(plan.index);
break;
case DocumentLayerOperation::reorder:
if (plan.mutates_document)
services.reorder_layer(plan.from_index, plan.to_index);
break;
case DocumentLayerOperation::remove:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer remove plan must mutate the document");
}
services.remove_layer(plan.index);
break;
case DocumentLayerOperation::set_opacity:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer opacity plan must mutate the document");
}
services.set_layer_opacity(plan.index, plan.opacity);
break;
case DocumentLayerOperation::set_visibility:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer visibility plan must mutate the document");
}
services.set_layer_visibility(plan.index, plan.flag);
break;
case DocumentLayerOperation::set_alpha_lock:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer alpha-lock plan must mutate the document");
}
services.set_layer_alpha_lock(plan.index, plan.flag);
break;
case DocumentLayerOperation::set_blend_mode:
if (!plan.mutates_document) {
return pp::foundation::Status::invalid_argument("layer blend-mode plan must mutate the document");
}
services.set_layer_blend_mode(plan.index, plan.blend_mode);
break;
case DocumentLayerOperation::set_highlight:
services.set_layer_highlight(plan.index, plan.flag);
break;
}
execute_document_layer_operation_side_effects(plan, services);
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_document_layer_merge_plan(
const DocumentLayerMergePlan& plan,
DocumentLayerMergeServices& services)
{
if (plan.from_index <= plan.to_index) {
return pp::foundation::Status::invalid_argument(
"layer merge source must be above the destination");
}
services.merge_layers(plan.from_index, plan.to_index, plan.create_history);
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
const DocumentLayerMenuPlan& plan,
DocumentLayerMenuServices& services)
{
switch (plan.action) {
case DocumentLayerMenuAction::clear_current_layer:
services.clear_current_layer();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::show_rename_dialog:
services.show_rename_dialog();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::merge_with_lower_layer:
services.merge_with_lower_layer(plan.from_index, plan.to_index);
return pp::foundation::Status::success();
case DocumentLayerMenuAction::show_merge_animated_not_supported:
services.show_merge_animated_not_supported();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::no_op_select_layer:
case DocumentLayerMenuAction::no_op_select_upper_layer:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document layer menu action");
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class PickedPathAction {
ignore_empty_path,
invoke_callback,
};
enum class DisplayFileAction {
ignore_empty_path,
open_external_file,
};
enum class VirtualKeyboardAction {
show_keyboard,
hide_keyboard,
};
enum class CursorVisibilityAction {
show_cursor,
hide_cursor,
};
enum class ClipboardReadAction {
read_text,
};
enum class ClipboardWriteAction {
write_text,
};
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
{
return path.empty()
? PickedPathAction::ignore_empty_path
: PickedPathAction::invoke_callback;
}
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
{
return path.empty()
? DisplayFileAction::ignore_empty_path
: DisplayFileAction::open_external_file;
}
[[nodiscard]] constexpr VirtualKeyboardAction plan_virtual_keyboard(bool visible) noexcept
{
return visible
? VirtualKeyboardAction::show_keyboard
: VirtualKeyboardAction::hide_keyboard;
}
[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept
{
return visible
? CursorVisibilityAction::show_cursor
: CursorVisibilityAction::hide_cursor;
}
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
{
return ClipboardReadAction::read_text;
}
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
{
return ClipboardWriteAction::write_text;
}
}

View File

@@ -0,0 +1,164 @@
#pragma once
#include "app_core/app_dialog.h"
#include "foundation/result.h"
#include <cstddef>
#include <limits>
#include <string_view>
namespace pp::app {
enum class RecordingStartAction {
start_thread,
no_op_already_running,
};
enum class RecordingStopAction {
stop_thread,
no_op_not_running,
};
struct RecordingClearPlan {
bool stop_running_recording = false;
bool delete_recorded_files = false;
int frame_count_after_clear = 0;
};
struct RecordingExportPlan {
std::size_t frame_count = 0;
int progress_total = 0;
};
struct RecordingWorkerIterationPlan {
bool continue_running = true;
bool encode_frame = false;
bool clear_dirty_stroke = false;
bool update_frame_label = false;
};
class RecordingServices {
public:
virtual ~RecordingServices() = default;
virtual void start_thread() = 0;
virtual void stop_thread() = 0;
virtual void delete_recorded_files() = 0;
virtual void set_frame_count(int frame_count) = 0;
virtual void update_frame_label() = 0;
virtual void begin_export(int progress_total) = 0;
virtual void write_mp4(std::string_view path) = 0;
virtual void end_export() = 0;
};
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
{
return is_running
? RecordingStartAction::no_op_already_running
: RecordingStartAction::start_thread;
}
[[nodiscard]] constexpr RecordingStopAction plan_recording_stop(bool is_running) noexcept
{
return is_running
? RecordingStopAction::stop_thread
: RecordingStopAction::no_op_not_running;
}
[[nodiscard]] constexpr RecordingClearPlan plan_recording_clear(
bool is_running,
bool platform_deletes_recorded_files) noexcept
{
return {
is_running,
platform_deletes_recorded_files,
0,
};
}
[[nodiscard]] constexpr RecordingExportPlan plan_recording_export(std::size_t frame_count) noexcept
{
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
return {
frame_count,
frame_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(frame_count),
};
}
[[nodiscard]] inline AppProgressDialogPlan plan_recording_export_progress_dialog(
const RecordingExportPlan& plan)
{
return plan_app_progress_dialog("Exporting MP4 movie", plan.progress_total);
}
[[nodiscard]] constexpr RecordingWorkerIterationPlan plan_recording_worker_iteration(
bool is_running_after_wake,
bool has_encoder,
bool has_canvas_document) noexcept
{
const bool encode = is_running_after_wake && has_encoder && has_canvas_document;
return {
.continue_running = is_running_after_wake,
.encode_frame = encode,
.clear_dirty_stroke = encode,
.update_frame_label = encode,
};
}
[[nodiscard]] inline pp::foundation::Status execute_recording_start_action(
RecordingStartAction action,
RecordingServices& services)
{
switch (action) {
case RecordingStartAction::start_thread:
services.start_thread();
return pp::foundation::Status::success();
case RecordingStartAction::no_op_already_running:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown recording start action");
}
[[nodiscard]] inline pp::foundation::Status execute_recording_stop_action(
RecordingStopAction action,
RecordingServices& services)
{
switch (action) {
case RecordingStopAction::stop_thread:
services.stop_thread();
return pp::foundation::Status::success();
case RecordingStopAction::no_op_not_running:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown recording stop action");
}
[[nodiscard]] inline pp::foundation::Status execute_recording_clear_plan(
const RecordingClearPlan& plan,
RecordingServices& services)
{
if (plan.stop_running_recording) {
services.stop_thread();
}
if (plan.delete_recorded_files) {
services.delete_recorded_files();
}
services.set_frame_count(plan.frame_count_after_clear);
services.update_frame_label();
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_recording_export_plan(
const RecordingExportPlan& plan,
RecordingServices& services,
std::string_view path)
{
services.begin_export(plan.progress_total);
services.write_mp4(path);
services.end_export();
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include "app_core/app_status.h"
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
struct DocumentResizeDialogState {
int current_resolution = 0;
std::string current_resolution_text;
int current_resolution_index = 0;
};
struct DocumentResizePlan {
int resolution = 0;
int width = 0;
int height = 0;
bool clears_history = false;
};
class DocumentResizeServices {
public:
virtual ~DocumentResizeServices() = default;
virtual void resize_document(int width, int height) = 0;
virtual void update_title() = 0;
virtual void clear_history() = 0;
};
[[nodiscard]] inline DocumentResizeDialogState make_document_resize_dialog_state(
int current_resolution)
{
const auto label = document_resolution_label(current_resolution);
const auto index = document_resolution_to_index(current_resolution);
std::string text = "Current: ";
text.append(label ? std::string_view(label.value()) : std::string_view("unknown"));
return {
current_resolution,
text,
index ? static_cast<int>(index.value()) : static_cast<int>(document_resolution_values.size()),
};
}
[[nodiscard]] inline pp::foundation::Result<DocumentResizePlan> plan_document_resize(
int selected_resolution_index)
{
const auto resolution = display_resolution_from_index(selected_resolution_index);
if (!resolution) {
return pp::foundation::Result<DocumentResizePlan>::failure(resolution.status());
}
const auto value = resolution.value();
return pp::foundation::Result<DocumentResizePlan>::success(
DocumentResizePlan {
value,
value,
value,
true,
});
}
[[nodiscard]] inline pp::foundation::Status execute_document_resize_plan(
const DocumentResizePlan& plan,
DocumentResizeServices& services)
{
if (plan.width <= 0 || plan.height <= 0) {
return pp::foundation::Status::out_of_range("resize dimensions must be positive");
}
services.resize_document(plan.width, plan.height);
services.update_title();
if (plan.clears_history) {
services.clear_history();
}
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,67 @@
#include "app_core/document_route.h"
#include <cctype>
#include <utility>
namespace pp::app {
namespace {
[[nodiscard]] bool is_extension_char(char value) noexcept
{
const auto ch = static_cast<unsigned char>(value);
return std::isalnum(ch) != 0 || value == '_';
}
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
{
std::string lowered;
lowered.reserve(value.size());
for (const char ch : value) {
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
}
return lowered;
}
}
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
{
const auto separator = path.find_last_of("/\\");
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
}
const auto dot = path.find_last_of('.');
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a file extension"));
}
const std::string_view extension = path.substr(dot + 1U);
for (const char ch : extension) {
if (!is_extension_char(ch)) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
}
}
auto lowered_extension = lowercase_ascii(extension);
auto kind = DocumentOpenKind::open_project;
if (lowered_extension == "abr") {
kind = DocumentOpenKind::import_abr;
} else if (lowered_extension == "ppbr") {
kind = DocumentOpenKind::import_ppbr;
}
return pp::foundation::Result<DocumentOpenRoute>::success(
DocumentOpenRoute {
.kind = kind,
.path = std::string(path),
.directory = std::string(path.substr(0U, separator)),
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
.extension = std::move(lowered_extension),
});
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
enum class DocumentOpenKind {
import_abr,
import_ppbr,
open_project,
};
struct DocumentOpenRoute {
DocumentOpenKind kind = DocumentOpenKind::open_project;
std::string path;
std::string directory;
std::string name;
std::string extension;
};
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
std::string_view path);
}

View File

@@ -0,0 +1 @@
#include "app_core/document_session.h"

View File

@@ -0,0 +1,567 @@
#pragma once
#include "app_core/app_dialog.h"
#include "app_core/document_route.h"
#include "foundation/result.h"
#include <array>
#include <cctype>
#include <cstdio>
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
enum class ProjectOpenDecision {
open_now,
prompt_discard_unsaved,
};
enum class CloseRequestDecision {
close_now,
show_unsaved_prompt,
wait_for_existing_prompt,
};
enum class DocumentSaveIntent {
save,
save_as,
save_version,
save_dirty_version,
};
enum class DocumentSaveDecision {
no_op,
show_save_dialog,
save_existing,
save_version,
};
enum class DocumentWorkflowDecision {
unavailable,
continue_now,
prompt_save_before_continue,
};
enum class DocumentFileWriteDecision {
save_now,
prompt_overwrite,
};
enum class DocumentOpenPlanAction {
open_project_now,
prompt_discard_unsaved_project,
prompt_import_abr,
prompt_import_ppbr,
};
enum class DocumentSessionPromptKind {
close_unsaved_document,
save_before_workflow_continue,
new_document_overwrite,
document_file_overwrite,
document_save_error,
};
class DocumentOpenServices {
public:
virtual ~DocumentOpenServices() = default;
virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0;
virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0;
virtual void open_project_now(const DocumentOpenRoute& route) = 0;
virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0;
};
class CloseRequestServices {
public:
virtual ~CloseRequestServices() = default;
virtual void request_close_now() = 0;
virtual void show_unsaved_close_prompt() = 0;
};
class DocumentSaveServices {
public:
virtual ~DocumentSaveServices() = default;
virtual void show_save_dialog() = 0;
virtual void save_existing_document() = 0;
virtual void save_document_version() = 0;
};
class DocumentWorkflowServices {
public:
virtual ~DocumentWorkflowServices() = default;
virtual void continue_workflow_now() = 0;
virtual void prompt_save_before_continue() = 0;
};
struct DocumentFileTarget {
std::string name;
std::string directory;
std::string path;
};
struct DocumentVersionTarget {
std::string name;
std::string path;
};
struct DocumentFileSavePlan {
DocumentFileTarget target;
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};
class DocumentFileSaveServices {
public:
virtual ~DocumentFileSaveServices() = default;
virtual void save_document_file(const DocumentFileSavePlan& plan) = 0;
virtual void prompt_overwrite_document_file(const DocumentFileSavePlan& plan) = 0;
};
class DocumentVersionSaveServices {
public:
virtual ~DocumentVersionSaveServices() = default;
virtual void save_document_version(const DocumentVersionTarget& target) = 0;
};
struct NewDocumentPlan {
DocumentFileTarget target;
int resolution = 0;
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};
class NewDocumentServices {
public:
virtual ~NewDocumentServices() = default;
virtual void create_new_document(const NewDocumentPlan& plan) = 0;
virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0;
};
[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt(
DocumentSessionPromptKind kind,
std::string_view document_name = {})
{
switch (kind) {
case DocumentSessionPromptKind::close_unsaved_document:
return plan_app_message_dialog(
"Unsaved document",
"Do you want to close without saving?",
true,
"Yes",
"No");
case DocumentSessionPromptKind::save_before_workflow_continue:
return plan_app_message_dialog(
"Unsaved document",
"Would you like to save this document before closing?",
true,
"Yes",
"No");
case DocumentSessionPromptKind::new_document_overwrite:
return plan_app_message_dialog(
"Warning",
"A document with this name already exists, continue?",
true);
case DocumentSessionPromptKind::document_file_overwrite:
{
std::string message = "Are you sure you want to overwrite ";
message += document_name;
message += "?";
return plan_app_message_dialog("Warning", message, true);
}
case DocumentSessionPromptKind::document_save_error:
return plan_app_message_dialog(
"Saving Error",
"There was a problem saving the document",
false);
}
return plan_app_message_dialog("Warning", "", false);
}
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
{
return has_unsaved_changes
? ProjectOpenDecision::prompt_discard_unsaved
: ProjectOpenDecision::open_now;
}
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
DocumentOpenKind kind,
bool has_unsaved_changes) noexcept
{
switch (kind) {
case DocumentOpenKind::import_abr:
return DocumentOpenPlanAction::prompt_import_abr;
case DocumentOpenKind::import_ppbr:
return DocumentOpenPlanAction::prompt_import_ppbr;
case DocumentOpenKind::open_project:
return has_unsaved_changes
? DocumentOpenPlanAction::prompt_discard_unsaved_project
: DocumentOpenPlanAction::open_project_now;
}
return DocumentOpenPlanAction::open_project_now;
}
[[nodiscard]] inline pp::foundation::Status execute_document_open_plan(
DocumentOpenPlanAction action,
const DocumentOpenRoute& route,
DocumentOpenServices& services)
{
switch (action) {
case DocumentOpenPlanAction::open_project_now:
if (route.kind != DocumentOpenKind::open_project) {
return pp::foundation::Status::invalid_argument("open-project action requires a project route");
}
services.open_project_now(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_discard_unsaved_project:
if (route.kind != DocumentOpenKind::open_project) {
return pp::foundation::Status::invalid_argument("discard prompt requires a project route");
}
services.prompt_discard_unsaved_project(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_import_abr:
if (route.kind != DocumentOpenKind::import_abr) {
return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route");
}
services.prompt_import_abr(route);
return pp::foundation::Status::success();
case DocumentOpenPlanAction::prompt_import_ppbr:
if (route.kind != DocumentOpenKind::import_ppbr) {
return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route");
}
services.prompt_import_ppbr(route);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document open action");
}
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
bool has_unsaved_changes,
bool close_prompt_already_open) noexcept
{
if (!has_unsaved_changes) {
return CloseRequestDecision::close_now;
}
return close_prompt_already_open
? CloseRequestDecision::wait_for_existing_prompt
: CloseRequestDecision::show_unsaved_prompt;
}
[[nodiscard]] inline pp::foundation::Status execute_close_request_decision(
CloseRequestDecision decision,
CloseRequestServices& services)
{
switch (decision) {
case CloseRequestDecision::close_now:
services.request_close_now();
return pp::foundation::Status::success();
case CloseRequestDecision::show_unsaved_prompt:
services.show_unsaved_close_prompt();
return pp::foundation::Status::success();
case CloseRequestDecision::wait_for_existing_prompt:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown close request decision");
}
[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
bool is_new_document,
bool has_unsaved_changes,
DocumentSaveIntent intent) noexcept
{
switch (intent) {
case DocumentSaveIntent::save:
if (is_new_document) {
return DocumentSaveDecision::show_save_dialog;
}
return has_unsaved_changes
? DocumentSaveDecision::save_existing
: DocumentSaveDecision::no_op;
case DocumentSaveIntent::save_as:
return DocumentSaveDecision::show_save_dialog;
case DocumentSaveIntent::save_version:
return is_new_document
? DocumentSaveDecision::show_save_dialog
: DocumentSaveDecision::save_version;
case DocumentSaveIntent::save_dirty_version:
if (is_new_document) {
return DocumentSaveDecision::show_save_dialog;
}
return has_unsaved_changes
? DocumentSaveDecision::save_version
: DocumentSaveDecision::no_op;
}
return DocumentSaveDecision::no_op;
}
[[nodiscard]] inline pp::foundation::Status execute_document_save_decision(
DocumentSaveDecision decision,
DocumentSaveServices& services)
{
switch (decision) {
case DocumentSaveDecision::no_op:
return pp::foundation::Status::success();
case DocumentSaveDecision::show_save_dialog:
services.show_save_dialog();
return pp::foundation::Status::success();
case DocumentSaveDecision::save_existing:
services.save_existing_document();
return pp::foundation::Status::success();
case DocumentSaveDecision::save_version:
services.save_document_version();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document save decision");
}
[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
bool has_canvas,
bool has_unsaved_changes) noexcept
{
if (!has_canvas) {
return DocumentWorkflowDecision::unavailable;
}
return has_unsaved_changes
? DocumentWorkflowDecision::prompt_save_before_continue
: DocumentWorkflowDecision::continue_now;
}
[[nodiscard]] inline pp::foundation::Status execute_document_workflow_decision(
DocumentWorkflowDecision decision,
DocumentWorkflowServices& services)
{
switch (decision) {
case DocumentWorkflowDecision::unavailable:
return pp::foundation::Status::success();
case DocumentWorkflowDecision::continue_now:
services.continue_workflow_now();
return pp::foundation::Status::success();
case DocumentWorkflowDecision::prompt_save_before_continue:
services.prompt_save_before_continue();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document workflow decision");
}
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
std::string_view work_directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentFileTarget target;
target.name = std::string(document_name);
target.directory = std::string(work_directory);
target.path.reserve(target.directory.size() + target.name.size() + 5);
target.path += target.directory;
target.path += "/";
target.path += target.name;
target.path += ".ppi";
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
}
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
bool target_exists) noexcept
{
return target_exists
? DocumentFileWriteDecision::prompt_overwrite
: DocumentFileWriteDecision::save_now;
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
std::string_view work_directory,
std::string_view document_name,
ExistsPredicate&& exists)
{
auto target = make_document_file_target(work_directory, document_name);
if (!target) {
return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
}
DocumentFileSavePlan plan;
plan.target = std::move(target.value());
plan.write_decision = plan_document_file_write(exists(plan.target.path));
return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Status execute_document_file_save_plan(
const DocumentFileSavePlan& plan,
DocumentFileSaveServices& services)
{
switch (plan.write_decision) {
case DocumentFileWriteDecision::save_now:
services.save_document_file(plan);
return pp::foundation::Status::success();
case DocumentFileWriteDecision::prompt_overwrite:
services.prompt_overwrite_document_file(plan);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document file save write decision");
}
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
{
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
return pp::foundation::Result<int>::failure(
pp::foundation::Status::out_of_range("document resolution index is out of range"));
}
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
std::string_view work_directory,
std::string_view document_name,
int resolution_index,
ExistsPredicate&& exists)
{
const auto resolution = document_resolution_from_index(resolution_index);
if (!resolution) {
return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
}
auto save_plan = plan_document_file_save(
work_directory,
document_name,
std::forward<ExistsPredicate>(exists));
if (!save_plan) {
return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
}
NewDocumentPlan plan;
plan.target = std::move(save_plan.value().target);
plan.resolution = resolution.value();
plan.write_decision = save_plan.value().write_decision;
return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Status execute_new_document_plan(
const NewDocumentPlan& plan,
NewDocumentServices& services)
{
switch (plan.write_decision) {
case DocumentFileWriteDecision::save_now:
services.create_new_document(plan);
return pp::foundation::Status::success();
case DocumentFileWriteDecision::prompt_overwrite:
services.prompt_overwrite_new_document(plan);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown new document write decision");
}
[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
{
const auto dot = document_name.rfind('.');
if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
return false;
}
const auto is_word = [](char ch) noexcept {
return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
};
return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
}
[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
{
int value = 0;
for (const char ch : suffix) {
if (ch < '0' || ch > '9') {
break;
}
value = value * 10 + (ch - '0');
}
return value;
}
[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
{
char suffix[4] {};
std::snprintf(suffix, sizeof(suffix), ".%02d", version);
std::string name;
name.reserve(base_name.size() + 3);
name += base_name;
name += suffix;
return name;
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
std::string_view directory,
std::string_view document_name,
ExistsPredicate&& exists)
{
if (directory.empty()) {
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::invalid_argument("directory must not be empty"));
}
if (document_name.empty()) {
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
int current = 0;
std::string_view base = document_name;
if (has_legacy_two_character_version_suffix(document_name)) {
const auto dot = document_name.rfind('.');
base = document_name.substr(0, dot);
current = legacy_version_number(document_name.substr(dot + 1));
}
for (int version = current + 1; version < 99; ++version) {
DocumentVersionTarget target;
target.name = make_legacy_version_name(base, version);
target.path.reserve(directory.size() + target.name.size() + 5);
target.path += directory;
target.path += "/";
target.path += target.name;
target.path += ".ppi";
if (!exists(target.path)) {
return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
}
}
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::out_of_range("no available document version target"));
}
[[nodiscard]] inline pp::foundation::Status execute_document_version_save(
const DocumentVersionTarget& target,
DocumentVersionSaveServices& services)
{
if (target.name.empty() || target.path.empty()) {
return pp::foundation::Status::invalid_argument("document version target requires a name and path");
}
services.save_document_version(target);
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class DocumentShareAction {
show_save_required_warning,
share_now,
};
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
{
return path.empty()
? DocumentShareAction::show_save_required_warning
: DocumentShareAction::share_now;
}
}

209
src/app_core/file_menu.h Normal file
View File

@@ -0,0 +1,209 @@
#pragma once
#include "app_core/document_export.h"
#include "app_core/document_session.h"
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class FileMenuCommand {
new_document,
import_image,
open_project,
browse_cloud,
save,
save_as,
save_version,
export_jpeg,
export_submenu,
share,
resize,
cloud_upload,
cloud_browse,
};
enum class FileMenuAction {
show_new_document_dialog,
pick_image_for_import,
pick_project_file,
show_cloud_browser_dialog,
save_document,
show_export_jpeg_dialog,
show_export_submenu,
share_document,
show_resize_dialog,
upload_to_cloud,
browse_cloud_documents,
};
struct FileMenuPlan {
FileMenuCommand command = FileMenuCommand::new_document;
FileMenuAction action = FileMenuAction::show_new_document_dialog;
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg;
};
class FileMenuServices {
public:
virtual ~FileMenuServices() = default;
virtual void show_new_document_dialog() = 0;
virtual void pick_image_for_import() = 0;
virtual void pick_project_file() = 0;
virtual void show_cloud_browser_dialog() = 0;
virtual void save_document(DocumentSaveIntent intent) = 0;
virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0;
virtual void show_export_submenu() = 0;
virtual void share_document() = 0;
virtual void show_resize_dialog() = 0;
virtual void upload_to_cloud() = 0;
virtual void browse_cloud_documents() = 0;
};
[[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept
{
FileMenuPlan plan;
plan.command = command;
switch (command) {
case FileMenuCommand::new_document:
plan.action = FileMenuAction::show_new_document_dialog;
break;
case FileMenuCommand::import_image:
plan.action = FileMenuAction::pick_image_for_import;
break;
case FileMenuCommand::open_project:
plan.action = FileMenuAction::pick_project_file;
break;
case FileMenuCommand::browse_cloud:
plan.action = FileMenuAction::show_cloud_browser_dialog;
break;
case FileMenuCommand::save:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save;
break;
case FileMenuCommand::save_as:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save_as;
break;
case FileMenuCommand::save_version:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save_version;
break;
case FileMenuCommand::export_jpeg:
plan.action = FileMenuAction::show_export_jpeg_dialog;
plan.export_kind = DocumentExportMenuKind::jpeg;
break;
case FileMenuCommand::export_submenu:
plan.action = FileMenuAction::show_export_submenu;
break;
case FileMenuCommand::share:
plan.action = FileMenuAction::share_document;
break;
case FileMenuCommand::resize:
plan.action = FileMenuAction::show_resize_dialog;
break;
case FileMenuCommand::cloud_upload:
plan.action = FileMenuAction::upload_to_cloud;
break;
case FileMenuCommand::cloud_browse:
plan.action = FileMenuAction::browse_cloud_documents;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Result<FileMenuCommand> parse_file_menu_command(
std::string_view command) noexcept
{
if (command == "new" || command == "new-document") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::new_document);
}
if (command == "import" || command == "import-image") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::import_image);
}
if (command == "open" || command == "open-project") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::open_project);
}
if (command == "browse") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::browse_cloud);
}
if (command == "save") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save);
}
if (command == "save-as") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_as);
}
if (command == "save-version") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_version);
}
if (command == "export" || command == "export-jpeg") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_jpeg);
}
if (command == "export-submenu") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_submenu);
}
if (command == "share") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::share);
}
if (command == "resize") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::resize);
}
if (command == "cloud-upload") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_upload);
}
if (command == "cloud-browse") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_browse);
}
return pp::foundation::Result<FileMenuCommand>::failure(
pp::foundation::Status::invalid_argument("unknown file menu command"));
}
[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan(
const FileMenuPlan& plan,
FileMenuServices& services)
{
switch (plan.action) {
case FileMenuAction::show_new_document_dialog:
services.show_new_document_dialog();
return pp::foundation::Status::success();
case FileMenuAction::pick_image_for_import:
services.pick_image_for_import();
return pp::foundation::Status::success();
case FileMenuAction::pick_project_file:
services.pick_project_file();
return pp::foundation::Status::success();
case FileMenuAction::show_cloud_browser_dialog:
services.show_cloud_browser_dialog();
return pp::foundation::Status::success();
case FileMenuAction::save_document:
services.save_document(plan.save_intent);
return pp::foundation::Status::success();
case FileMenuAction::show_export_jpeg_dialog:
services.show_export_jpeg_dialog(plan.export_kind);
return pp::foundation::Status::success();
case FileMenuAction::show_export_submenu:
services.show_export_submenu();
return pp::foundation::Status::success();
case FileMenuAction::share_document:
services.share_document();
return pp::foundation::Status::success();
case FileMenuAction::show_resize_dialog:
services.show_resize_dialog();
return pp::foundation::Status::success();
case FileMenuAction::upload_to_cloud:
services.upload_to_cloud();
return pp::foundation::Status::success();
case FileMenuAction::browse_cloud_documents:
services.browse_cloud_documents();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown file menu action");
}
} // namespace pp::app

209
src/app_core/grid_ui.h Normal file
View File

@@ -0,0 +1,209 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
enum class GridUiOperation {
request_heightmap_pick,
load_heightmap,
clear_heightmap,
reload_heightmap,
render_lightmap,
commit_heightmap,
};
struct GridUiPlan {
GridUiOperation operation = GridUiOperation::request_heightmap_pick;
std::string path;
int texture_resolution = 0;
int sample_count = 0;
bool opens_picker = false;
bool loads_heightmap = false;
bool clears_heightmap = false;
bool renders_lightmap = false;
bool commits_heightmap = false;
bool updates_preview = false;
bool updates_ground_opacity = false;
bool updates_shading_mode = false;
bool shows_unsupported_message = false;
bool shows_progress = false;
bool mutates_grid_state = false;
};
class GridUiServices {
public:
virtual ~GridUiServices() = default;
virtual void request_heightmap_pick() = 0;
virtual pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) = 0;
virtual void clear_heightmap(bool updates_preview) = 0;
virtual void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) = 0;
virtual void commit_heightmap(bool updates_ground_opacity) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
{
if (texture_resolution <= 0 || texture_resolution > 16384) {
return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept
{
if (sample_count <= 0 || sample_count > 4096) {
return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::request_heightmap_pick;
plan.opens_picker = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
{
if (path.empty()) {
return pp::foundation::Result<GridUiPlan>::failure(
pp::foundation::Status::invalid_argument("heightmap path must not be empty"));
}
GridUiPlan plan;
plan.operation = GridUiOperation::load_heightmap;
plan.path = std::string(path);
plan.loads_heightmap = true;
plan.updates_preview = true;
plan.updates_ground_opacity = true;
plan.mutates_grid_state = true;
return pp::foundation::Result<GridUiPlan>::success(std::move(plan));
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::clear_heightmap;
plan.clears_heightmap = true;
plan.updates_preview = has_heightmap;
plan.mutates_grid_state = has_heightmap;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
{
auto plan = plan_grid_heightmap_load(path);
if (!plan) {
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
}
plan.value().operation = GridUiOperation::reload_heightmap;
plan.value().updates_ground_opacity = false;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_lightmap_render(
bool has_heightmap,
bool supports_float32,
bool supports_float16,
int texture_resolution,
int sample_count)
{
const auto texture_status = validate_grid_texture_resolution(texture_resolution);
if (!texture_status.ok()) {
return pp::foundation::Result<GridUiPlan>::failure(texture_status);
}
const auto sample_status = validate_grid_lightmap_samples(sample_count);
if (!sample_status.ok()) {
return pp::foundation::Result<GridUiPlan>::failure(sample_status);
}
GridUiPlan plan;
plan.operation = GridUiOperation::render_lightmap;
plan.texture_resolution = texture_resolution;
plan.sample_count = sample_count;
if (!supports_float32 && !supports_float16) {
plan.shows_unsupported_message = true;
return pp::foundation::Result<GridUiPlan>::success(plan);
}
plan.renders_lightmap = has_heightmap;
plan.shows_progress = has_heightmap;
plan.updates_shading_mode = has_heightmap;
plan.mutates_grid_state = has_heightmap;
return pp::foundation::Result<GridUiPlan>::success(plan);
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::commit_heightmap;
plan.commits_heightmap = has_canvas;
plan.updates_ground_opacity = has_canvas;
plan.mutates_grid_state = has_canvas;
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_grid_ui_plan(
const GridUiPlan& plan,
GridUiServices& services)
{
switch (plan.operation) {
case GridUiOperation::request_heightmap_pick:
if (!plan.opens_picker) {
return pp::foundation::Status::invalid_argument("grid heightmap pick plan must open a picker");
}
services.request_heightmap_pick();
return pp::foundation::Status::success();
case GridUiOperation::load_heightmap:
case GridUiOperation::reload_heightmap:
if (!plan.loads_heightmap || plan.path.empty()) {
return pp::foundation::Status::invalid_argument("grid heightmap load plan must provide a path");
}
return services.load_heightmap(plan.path, plan.updates_ground_opacity);
case GridUiOperation::clear_heightmap:
if (!plan.clears_heightmap) {
return pp::foundation::Status::invalid_argument("grid heightmap clear plan must clear heightmap state");
}
services.clear_heightmap(plan.updates_preview);
return pp::foundation::Status::success();
case GridUiOperation::render_lightmap:
{
const auto texture_status = validate_grid_texture_resolution(plan.texture_resolution);
if (!texture_status.ok()) {
return texture_status;
}
const auto sample_status = validate_grid_lightmap_samples(plan.sample_count);
if (!sample_status.ok()) {
return sample_status;
}
if (!plan.shows_unsupported_message && !plan.renders_lightmap) {
return pp::foundation::Status::success();
}
services.render_lightmap(plan.shows_unsupported_message, plan.renders_lightmap);
return pp::foundation::Status::success();
}
case GridUiOperation::commit_heightmap:
if (plan.commits_heightmap) {
services.commit_heightmap(plan.updates_ground_opacity);
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown grid UI operation");
}
} // namespace pp::app

161
src/app_core/history_ui.h Normal file
View File

@@ -0,0 +1,161 @@
#pragma once
#include "foundation/result.h"
namespace pp::app {
enum class HistoryUiOperation {
undo,
redo,
clear,
};
struct HistoryUiPlan {
HistoryUiOperation operation = HistoryUiOperation::undo;
int undo_count = 0;
int redo_count = 0;
int memory_bytes = 0;
bool invokes_undo = false;
bool invokes_redo = false;
bool clears_history = false;
bool updates_memory_label = false;
bool updates_title = false;
bool no_op = false;
};
class HistoryUiServices {
public:
virtual ~HistoryUiServices() = default;
virtual void invoke_undo() = 0;
virtual void invoke_redo() = 0;
virtual void clear_history() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
{
if (value < 0) {
return pp::foundation::Status::out_of_range(message);
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
{
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::undo;
plan.undo_count = undo_count;
if (undo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_undo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
{
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::redo;
plan.redo_count = redo_count;
if (redo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_redo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
int undo_count,
int redo_count,
int memory_bytes)
{
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
}
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
}
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::clear;
plan.undo_count = undo_count;
plan.redo_count = redo_count;
plan.memory_bytes = memory_bytes;
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.clears_history = true;
plan.updates_memory_label = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
const HistoryUiPlan& plan,
HistoryUiServices& services)
{
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return undo_status;
}
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return redo_status;
}
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return memory_status;
}
if (plan.no_op) {
return pp::foundation::Status::success();
}
switch (plan.operation) {
case HistoryUiOperation::undo:
if (plan.invokes_undo) {
services.invoke_undo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::redo:
if (plan.invokes_redo) {
services.invoke_redo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::clear:
if (plan.clears_history) {
services.clear_history();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown history operation");
}
} // namespace pp::app

211
src/app_core/main_toolbar.h Normal file
View File

@@ -0,0 +1,211 @@
#pragma once
#include "app_core/app_dialog.h"
#include "app_core/document_canvas.h"
#include "app_core/history_ui.h"
#include "foundation/result.h"
#include <string>
namespace pp::app {
enum class MainToolbarCommand {
open_document,
save_document,
undo,
redo,
clear_history,
clear_canvas,
show_message_box,
show_settings,
};
enum class MainToolbarAction {
show_open_dialog,
show_save_dialog,
invoke_undo,
invoke_redo,
clear_history,
clear_canvas,
show_message_box,
show_settings_dialog,
no_op_unavailable,
};
struct MainToolbarPlan {
MainToolbarCommand command = MainToolbarCommand::open_document;
MainToolbarAction action = MainToolbarAction::show_open_dialog;
std::string label;
bool requires_canvas = false;
bool updates_memory_label = false;
bool updates_title = false;
bool records_undo = false;
bool marks_unsaved = false;
bool no_op = false;
HistoryUiPlan history;
DocumentCanvasClearPlan canvas_clear;
};
class MainToolbarServices {
public:
virtual ~MainToolbarServices() = default;
virtual void show_open_dialog() = 0;
virtual void show_save_dialog() = 0;
virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
virtual void clear_history(const HistoryUiPlan& plan) = 0;
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
virtual void show_message_box() = 0;
virtual void show_settings_dialog() = 0;
};
[[nodiscard]] inline AppMessageDialogPlan plan_main_toolbar_message_dialog()
{
return plan_app_message_dialog(
"Just a test message",
"Longer description for the error or the message.",
true);
}
[[nodiscard]] inline pp::foundation::Result<MainToolbarPlan> plan_main_toolbar_command(
MainToolbarCommand command,
int undo_count = 0,
int redo_count = 0,
int memory_bytes = 0,
bool has_canvas = true)
{
MainToolbarPlan plan;
plan.command = command;
switch (command) {
case MainToolbarCommand::open_document:
plan.action = MainToolbarAction::show_open_dialog;
plan.label = "Open";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::save_document:
plan.action = MainToolbarAction::show_save_dialog;
plan.label = "Save";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::undo:
{
const auto history = plan_history_undo(undo_count);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().invokes_undo
? MainToolbarAction::invoke_undo
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::redo:
{
const auto history = plan_history_redo(redo_count);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().invokes_redo
? MainToolbarAction::invoke_redo
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::clear_history:
{
const auto history = plan_history_clear(undo_count, redo_count, memory_bytes);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().clears_history
? MainToolbarAction::clear_history
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::clear_canvas:
{
const auto clear = plan_document_canvas_clear(has_canvas);
if (!clear) {
return pp::foundation::Result<MainToolbarPlan>::failure(clear.status());
}
plan.action = clear.value().clears_canvas
? MainToolbarAction::clear_canvas
: MainToolbarAction::no_op_unavailable;
plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)";
plan.requires_canvas = true;
plan.records_undo = clear.value().records_undo;
plan.marks_unsaved = clear.value().marks_unsaved;
plan.no_op = clear.value().no_op;
plan.canvas_clear = clear.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::show_message_box:
plan.action = MainToolbarAction::show_message_box;
plan.label = "Show Message Box";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::show_settings:
plan.action = MainToolbarAction::show_settings_dialog;
plan.label = "Settings";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
return pp::foundation::Result<MainToolbarPlan>::failure(
pp::foundation::Status::invalid_argument("unknown main toolbar command"));
}
[[nodiscard]] inline pp::foundation::Status execute_main_toolbar_plan(
const MainToolbarPlan& plan,
MainToolbarServices& services)
{
switch (plan.action) {
case MainToolbarAction::show_open_dialog:
services.show_open_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::show_save_dialog:
services.show_save_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::invoke_undo:
services.invoke_undo(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::invoke_redo:
services.invoke_redo(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::clear_history:
services.clear_history(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::clear_canvas:
services.clear_canvas(plan.canvas_clear);
return pp::foundation::Status::success();
case MainToolbarAction::show_message_box:
services.show_message_box();
return pp::foundation::Status::success();
case MainToolbarAction::show_settings_dialog:
services.show_settings_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown main toolbar action");
}
} // namespace pp::app

302
src/app_core/quick_ui.h Normal file
View File

@@ -0,0 +1,302 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
namespace pp::app {
enum class QuickUiSlotKind {
brush,
color,
};
enum class QuickUiOperation {
select_slot,
open_slot_popup,
restore_state,
reset_state,
};
struct QuickUiPlan {
QuickUiOperation operation = QuickUiOperation::select_slot;
QuickUiSlotKind slot_kind = QuickUiSlotKind::brush;
int slot_index = 0;
int previous_index = 0;
int brush_index = 0;
int color_index = 0;
int slot_count = 0;
bool fire_event = false;
bool updates_selection = false;
bool opens_brush_popup = false;
bool opens_color_picker = false;
bool invokes_change_callback = false;
bool restores_slots = false;
bool resets_slots = false;
bool redraws_brush_previews = false;
bool mutates_quick_state = false;
};
struct QuickSliderPreviewInput {
bool ui_rtl = false;
float slider_x = 0.0F;
float slider_y = 0.0F;
float slider_height = 0.0F;
float zoom = 1.0F;
bool has_pen_mode = false;
bool has_line_mode = false;
};
struct QuickSliderPreviewPlan {
float cursor_x = 0.0F;
float cursor_y = 0.0F;
bool updates_pen_mode = false;
bool updates_line_mode = false;
bool draws_tip = false;
bool disables_pen_outline = false;
bool redraws_brush_preview = false;
bool invokes_change_callback = false;
};
class QuickUiServices {
public:
virtual ~QuickUiServices() = default;
virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0;
virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0;
virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0;
virtual void reset_state(bool fire_event) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_quick_slider_float(float value) noexcept
{
if (!std::isfinite(value)) {
return pp::foundation::Status::invalid_argument("quick slider preview value must be finite");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
{
if (slot_count <= 0) {
return pp::foundation::Status::out_of_range("quick slot count must be greater than zero");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept
{
const auto count_status = validate_quick_slot_count(slot_count);
if (!count_status.ok()) {
return count_status;
}
if (slot_index < 0 || slot_index >= slot_count) {
return pp::foundation::Status::out_of_range("quick slot index is out of range");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<QuickSliderPreviewPlan> plan_quick_slider_preview(
const QuickSliderPreviewInput& input)
{
const auto x_status = validate_quick_slider_float(input.slider_x);
if (!x_status.ok()) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(x_status);
}
const auto y_status = validate_quick_slider_float(input.slider_y);
if (!y_status.ok()) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(y_status);
}
const auto height_status = validate_quick_slider_float(input.slider_height);
if (!height_status.ok()) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(height_status);
}
if (input.slider_height < 0.0F) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
pp::foundation::Status::invalid_argument("quick slider preview height must not be negative"));
}
const auto zoom_status = validate_quick_slider_float(input.zoom);
if (!zoom_status.ok()) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(zoom_status);
}
if (input.zoom <= 0.0F) {
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
pp::foundation::Status::invalid_argument("quick slider preview zoom must be positive"));
}
const float offset = input.ui_rtl ? -100.0F : 100.0F;
QuickSliderPreviewPlan plan;
plan.cursor_x = (input.slider_x + offset) * input.zoom;
plan.cursor_y = (input.slider_y + input.slider_height * 0.5F) * input.zoom;
plan.updates_pen_mode = input.has_pen_mode;
plan.updates_line_mode = input.has_line_mode;
plan.draws_tip = input.has_pen_mode || input.has_line_mode;
plan.disables_pen_outline = input.has_pen_mode;
plan.redraws_brush_preview = true;
plan.invokes_change_callback = true;
return pp::foundation::Result<QuickSliderPreviewPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
QuickUiSlotKind slot_kind,
int current_index,
int clicked_index,
int slot_count)
{
const auto current_status = validate_quick_slot_index(current_index, slot_count);
if (!current_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(current_status);
}
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
if (!clicked_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(clicked_status);
}
QuickUiPlan plan;
plan.slot_kind = slot_kind;
plan.slot_index = clicked_index;
plan.previous_index = current_index;
plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0;
plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0;
plan.slot_count = slot_count;
if (clicked_index != current_index) {
plan.operation = QuickUiOperation::select_slot;
plan.updates_selection = true;
plan.invokes_change_callback = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
plan.operation = QuickUiOperation::open_slot_popup;
plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush;
plan.opens_color_picker = slot_kind == QuickUiSlotKind::color;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_restore(
int brush_index,
int color_index,
int slot_count,
bool fire_event)
{
const auto brush_status = validate_quick_slot_index(brush_index, slot_count);
if (!brush_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(brush_status);
}
const auto color_status = validate_quick_slot_index(color_index, slot_count);
if (!color_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(color_status);
}
QuickUiPlan plan;
plan.operation = QuickUiOperation::restore_state;
plan.brush_index = brush_index;
plan.color_index = color_index;
plan.slot_count = slot_count;
plan.fire_event = fire_event;
plan.updates_selection = true;
plan.invokes_change_callback = fire_event;
plan.restores_slots = true;
plan.redraws_brush_previews = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_reset(
int slot_count,
bool fire_event)
{
const auto count_status = validate_quick_slot_count(slot_count);
if (!count_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(count_status);
}
QuickUiPlan plan;
plan.operation = QuickUiOperation::reset_state;
plan.brush_index = 0;
plan.color_index = 0;
plan.slot_count = slot_count;
plan.fire_event = fire_event;
plan.updates_selection = true;
plan.invokes_change_callback = fire_event;
plan.resets_slots = true;
plan.redraws_brush_previews = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan(
const QuickUiPlan& plan,
QuickUiServices& services)
{
switch (plan.operation) {
case QuickUiOperation::select_slot:
if (!plan.updates_selection) {
return pp::foundation::Status::invalid_argument("quick select plan must update selection");
}
{
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback);
return pp::foundation::Status::success();
case QuickUiOperation::open_slot_popup:
if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) {
return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup");
}
if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) {
return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker");
}
{
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.open_slot_popup(plan.slot_kind, plan.slot_index);
return pp::foundation::Status::success();
case QuickUiOperation::restore_state:
if (!plan.restores_slots) {
return pp::foundation::Status::invalid_argument("quick restore plan must restore slots");
}
{
const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count);
if (!brush_status.ok()) {
return brush_status;
}
const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count);
if (!color_status.ok()) {
return color_status;
}
}
services.restore_state(plan.brush_index, plan.color_index, plan.fire_event);
return pp::foundation::Status::success();
case QuickUiOperation::reset_state:
if (!plan.resets_slots) {
return pp::foundation::Status::invalid_argument("quick reset plan must reset slots");
}
{
const auto status = validate_quick_slot_count(plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.reset_state(plan.fire_event);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown quick UI operation");
}
} // namespace pp::app

185
src/app_core/tools_menu.h Normal file
View File

@@ -0,0 +1,185 @@
#pragma once
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class ToolsMenuCommand {
panels,
options,
clear_grids,
reset_camera,
shortcuts,
sonarpen,
};
enum class ToolsMenuAction {
show_panels_submenu,
show_options_submenu,
clear_grid_overlays,
reset_camera,
show_shortcuts_dialog,
start_sonarpen,
no_op_unavailable,
};
enum class ToolsPanel {
presets,
color,
color_advanced,
layers,
brush,
grids,
animation,
};
enum class ToolsPanelAction {
open_floating_panel,
no_op_already_visible,
};
struct ToolsMenuPlan {
ToolsMenuCommand command = ToolsMenuCommand::panels;
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
std::string_view label;
bool closes_root_popup = false;
};
struct ToolsPanelPlan {
ToolsPanel panel = ToolsPanel::presets;
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
std::string_view title;
int width = 0;
int height = 0;
int min_width = 0;
int min_height = 0;
bool droppable = true;
bool hides_embedded_title = false;
};
class ToolsMenuServices {
public:
virtual ~ToolsMenuServices() = default;
virtual void show_panels_submenu() = 0;
virtual void show_options_submenu() = 0;
virtual void clear_grid_overlays() = 0;
virtual void reset_camera() = 0;
virtual void show_shortcuts_dialog() = 0;
virtual void start_sonarpen() = 0;
};
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
ToolsMenuCommand command,
bool sonarpen_available = false) noexcept
{
switch (command) {
case ToolsMenuCommand::panels:
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
case ToolsMenuCommand::options:
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
case ToolsMenuCommand::clear_grids:
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
case ToolsMenuCommand::reset_camera:
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
case ToolsMenuCommand::shortcuts:
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
case ToolsMenuCommand::sonarpen:
return {
command,
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
"SonarPen",
sonarpen_available,
};
}
return { command, ToolsMenuAction::no_op_unavailable, "", false };
}
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
ToolsPanel panel,
bool already_visible) noexcept
{
ToolsPanelPlan plan;
plan.panel = panel;
plan.action = already_visible
? ToolsPanelAction::no_op_already_visible
: ToolsPanelAction::open_floating_panel;
switch (panel) {
case ToolsPanel::presets:
plan.title = "Brushes";
plan.height = 300;
plan.min_height = 300;
plan.min_width = 100;
break;
case ToolsPanel::color:
plan.title = "Color Picker";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::color_advanced:
plan.title = "Color Picker";
plan.width = 300;
plan.height = 300;
break;
case ToolsPanel::layers:
plan.title = "Layers";
plan.height = 300;
plan.min_height = 100;
plan.hides_embedded_title = true;
break;
case ToolsPanel::brush:
plan.title = "Brush Settings";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::grids:
plan.title = "Grid";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::animation:
plan.title = "Animation";
plan.width = 500;
plan.height = 300;
plan.droppable = false;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
const ToolsMenuPlan& plan,
ToolsMenuServices& services)
{
switch (plan.action) {
case ToolsMenuAction::show_panels_submenu:
services.show_panels_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::show_options_submenu:
services.show_options_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::clear_grid_overlays:
services.clear_grid_overlays();
return pp::foundation::Status::success();
case ToolsMenuAction::reset_camera:
services.reset_camera();
return pp::foundation::Status::success();
case ToolsMenuAction::show_shortcuts_dialog:
services.show_shortcuts_dialog();
return pp::foundation::Status::success();
case ToolsMenuAction::start_sonarpen:
services.start_sonarpen();
return pp::foundation::Status::success();
case ToolsMenuAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown tools menu action");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,520 +1,694 @@
#include "pch.h"
#include "app.h"
#include "app_core/app_frame.h"
#include "app_core/app_input.h"
#include "app_core/document_platform_io.h"
#include "app_core/document_sharing.h"
#include "platform_api/platform_services.h"
#include "platform_legacy/legacy_platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef __ANDROID__
void displayKeyboard(bool pShow);
void android_pick_file(std::function<void(std::string)> callback);
void android_pick_file_save(std::function<void(std::string)> callback);
std::string android_get_clipboard();
bool android_set_clipboard(const std::string& s);
#elif _WIN32
std::string win32_open_file(const char* filter);
std::string win32_save_file(const char* filter);
std::string win32_open_dir();
void win32_show_cursor(bool visible);
bool win32_clipboard_set_text(const std::string & s);
std::string win32_clipboard_get_text();
#elif __APPLE__
#elif __LINUX__
#include <tinyfiledialogs.h>
#elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_pick_file_save(const std::string& path,
const std::string& name, std::function<void(bool)> callback);
void webgl_sync();
#endif
namespace {
[[nodiscard]] GLint rgba8_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
{
const auto action = pp::app::plan_virtual_keyboard(visible);
return visible
? action == pp::app::VirtualKeyboardAction::show_keyboard
: action == pp::app::VirtualKeyboardAction::hide_keyboard;
}
[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept
{
const auto action = pp::app::plan_cursor_visibility(visible);
return visible
? action == pp::app::CursorVisibilityAction::show_cursor
: action == pp::app::CursorVisibilityAction::hide_cursor;
}
}
namespace {
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
{
if (App::I)
{
if (auto* services = App::I->platform_services())
return *services;
}
return pp::platform::legacy::platform_services();
}
}
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
{
platform_services_ = services;
}
pp::platform::PlatformServices* App::platform_services() const noexcept
{
return platform_services_;
}
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
{
return active_platform_services().prepare_storage_paths();
}
std::string App::clipboard_get_text()
{
#if _WIN32
return win32_clipboard_get_text();
#elif __IOS__
return [ios_view clipboard_get_string];
#elif __OSX__
return [osx_view clipboard_get_string];
#elif __ANDROID__
return android_get_clipboard();
#endif
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
return {};
return active_platform_services().clipboard_text();
}
bool App::clipboard_set_text(const std::string& s)
{
#if _WIN32
return win32_clipboard_set_text(s);
#elif __IOS__
return [ios_view clipboard_set_string:s];
#elif __OSX__
return [osx_view clipboard_set_string:s];
#elif __ANDROID__
return android_set_clipboard(s);
#endif
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
return false;
return active_platform_services().set_clipboard_text(s);
}
void App::stacktrace()
{
#if __OSX__
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
#endif
active_platform_services().log_stacktrace();
}
void App::crash_test()
{
#ifdef __IOS__
[ios_view crash];
#elif __OSX__
[osx_view hockeyapp_crash];
#elif defined(_WIN32)
__debugbreak();
#elif defined(__ANDROID__)
int *x = nullptr; *x = 42;
LOG("%d", *x);
#endif
active_platform_services().trigger_crash_test();
}
void App::tick(float dt)
{
if (auto* main = layout_designer[main_id])
const auto tick_plan = pp::app::plan_app_frame_tick(
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
if (auto* main = layout_designer[main_id]; tick_plan.tick_designer_layout && main)
main->tick(dt);
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; tick_plan.tick_main_layout && main)
main->tick(dt);
}
void App::resize(float w, float h)
{
LOG("App::resize %d %d", (int)w, (int)h);
uirtt.create(w, h, -1, GL_RGBA8, true);
redraw = true;
width = w;
height = h;
const auto resize_plan = pp::app::plan_app_resize(w, h);
if (!resize_plan) {
LOG("App resize plan failed: %s", resize_plan.status().message);
return;
}
if (resize_plan.value().recreate_ui_render_target) {
uirtt.create(
resize_plan.value().render_target_width,
resize_plan.value().render_target_height,
-1,
rgba8_internal_format(),
true);
}
redraw = resize_plan.value().request_redraw;
width = resize_plan.value().width;
height = resize_plan.value().height;
}
void App::show_cursor()
{
#ifdef _WIN32
win32_show_cursor(true);
#elif __OSX__
[osx_view show_cursor:true];
#endif
if (!should_dispatch_cursor_visibility(true))
return;
active_platform_services().set_cursor_visible(true);
}
void App::hide_cursor()
{
#ifdef _WIN32
win32_show_cursor(false);
#elif __OSX__
[osx_view show_cursor:false];
#endif
if (!should_dispatch_cursor_visibility(false))
return;
active_platform_services().set_cursor_visible(false);
}
void App::showKeyboard()
{
LOG("show keyboard");
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view show_keyboard];
});
#elif __ANDROID__
displayKeyboard(true);
#endif
if (!should_dispatch_keyboard_visibility(true))
return;
active_platform_services().set_virtual_keyboard_visible(true);
}
void App::hideKeyboard()
{
LOG("hide keyboard");
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view hide_keyboard];
});
#elif __ANDROID__
displayKeyboard(false);
#endif
if (!should_dispatch_keyboard_visibility(false))
return;
active_platform_services().set_virtual_keyboard_visible(false);
}
void App::pick_image(std::function<void(std::string path)> callback)
{
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view pick_photo:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
std::string path = [osx_view pick_file:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file(callback);
#elif _WIN32
std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
if (!path.empty())
callback(path);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
callback(p);
#elif __WEB__
webgl_pick_file(callback);
#endif
active_platform_services().pick_image(std::move(callback));
}
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
{
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
[ios_view pick_file:fileTypes then:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [osx_view pick_file:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file(callback);
#elif _WIN32
std::string filter = "Supported Files (";
bool first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
first_type = false;
}
filter.append(")");
filter.push_back(0);
first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : ";") + "*." + t);
first_type = false;
}
filter.push_back(0);
std::string path = win32_open_file(filter.c_str());
if (!path.empty())
callback(path);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
callback(p);
#elif __WEB__
webgl_pick_file(callback);
#endif
active_platform_services().pick_file(std::move(types), std::move(callback));
}
#if __IOS__
void App::pick_file_save(const std::string& type, const std::string& default_name,
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
{
redraw = true;
std::string ext = "." + type;
std::string path = tmp_path + "/" + default_name + ext;
std::thread([=]{
writer(path);
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view pick_file_save:path];
});
callback(path, true);
}).detach();
const auto target = active_platform_services().prepare_writable_file(type, default_name, data_path, tmp_path);
if (target.path.empty())
{
callback({}, false);
return;
}
LOG("App::pick_file_save %s", target.path.c_str());
auto write_and_save = [=] {
writer(target.path);
save_prepared_file(target.path, target.suggested_name, callback);
};
if (target.write_on_background_thread)
std::thread(write_and_save).detach();
else
write_and_save();
}
#elif __WEB__
void App::pick_file_save(const std::string& type, const std::string& default_name,
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
{
redraw = true;
auto path = data_path + "/" + default_name + "." + type;
LOG("App::pick_file_save %s", path.c_str());
writer(path);
webgl_pick_file_save(path, default_name + "." + type, callback);
}
#else
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
{
redraw = true;
#if __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
//NSArray* fileTypes = [NSArray arrayWithObjects:@"ppi", @"PPI", nil];
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [osx_view pick_file_save:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file_save(callback);
#elif _WIN32
std::string filter = "Supported Files (";
bool first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
first_type = false;
}
filter.append(")");
filter.push_back(0);
first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : ";") + "*." + t);
first_type = false;
}
filter.push_back(0);
std::string path = win32_save_file(filter.c_str());
if (!path.empty())
callback(path);
#endif
active_platform_services().pick_save_file(std::move(types), std::move(callback));
}
bool App::uses_prepared_file_writes() const
{
return active_platform_services().uses_prepared_file_writes();
}
bool App::uses_work_directory_document_export_collections() const
{
return active_platform_services().uses_work_directory_document_export_collections();
}
bool App::disables_network_tls_verification() const
{
return active_platform_services().disables_network_tls_verification();
}
bool App::uses_ppbr_export_data_directory_override() const
{
return active_platform_services().uses_ppbr_export_data_directory_override();
}
bool App::platform_supports_sonarpen() const
{
return active_platform_services().supports_sonarpen();
}
void App::start_platform_sonarpen()
{
active_platform_services().start_sonarpen();
}
int App::default_canvas_resolution() const
{
return active_platform_services().default_canvas_resolution();
}
bool App::draws_canvas_tip_for_input(kEventSource source, kEventType type) const
{
return active_platform_services().draws_canvas_tip_for_pointer(
source == kEventSource::Mouse,
source == kEventSource::Stylus,
type == kEventType::MouseUpL);
}
float App::adjust_canvas_input_pressure(float pressure) const
{
return active_platform_services().adjust_canvas_input_pressure(pressure);
}
#endif
void App::pick_dir(std::function<void(std::string path)> callback)
{
redraw = true;
#ifdef __IOS__
// NOT IMPLEMENTED
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
std::string path = [osx_view pick_dir];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
// NOT IMPLEMENTED
#elif _WIN32
// TODO: to be implemented
std::string path = win32_open_dir();
if (!path.empty())
callback(path);
#endif
active_platform_services().pick_directory(std::move(callback));
}
bool App::supports_working_directory_picker() const
{
return active_platform_services().supports_working_directory_picker();
}
std::string App::format_working_directory_path(std::string_view path) const
{
return active_platform_services().format_working_directory_path(path);
}
void App::display_file(std::string path)
{
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view display_file:path];
});
#elif __OSX__
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:path.c_str()]];
// dispatch_async(dispatch_get_main_queue(), ^{
// std::string path = [osx_view pick_file];
// if (!path.empty())
// callback(path);
// });
#elif __ANDROID__
//displayKeyboard(and_app, false);
#elif _WIN32
// std::string path = win32_open_file();
// if (!path.empty())
// callback(path);
#endif
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
return;
active_platform_services().display_file(path);
}
void App::share_file(std::string path)
{
if (path.empty())
const auto plan = pp::app::plan_document_share(path);
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
{
message_box("Sharing failed", "Please save the document before sharing it.");
return;
}
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
});
#elif __ANDROID__
#elif _WIN32
// not implemented
#endif
active_platform_services().share_file(path);
}
void App::request_app_close()
{
active_platform_services().request_app_close();
}
bool App::start_platform_vr_mode()
{
return active_platform_services().start_vr_mode();
}
void App::stop_platform_vr_mode()
{
active_platform_services().stop_vr_mode();
}
void App::attach_ui_thread()
{
active_platform_services().attach_ui_thread();
}
void App::detach_ui_thread()
{
active_platform_services().detach_ui_thread();
}
void App::acquire_render_context()
{
active_platform_services().acquire_render_context();
}
void App::release_render_context()
{
active_platform_services().release_render_context();
}
void App::present_render_context()
{
active_platform_services().present_render_context();
}
void App::bind_default_render_target()
{
active_platform_services().bind_default_render_target();
}
void App::bind_main_render_target()
{
active_platform_services().bind_main_render_target();
}
void App::apply_render_platform_hints()
{
active_platform_services().apply_render_platform_hints();
}
void App::install_render_debug_callback()
{
active_platform_services().install_render_debug_callback();
}
void App::begin_render_capture_frame()
{
active_platform_services().begin_render_capture_frame();
}
void App::end_render_capture_frame()
{
active_platform_services().end_render_capture_frame();
}
bool App::platform_deletes_recorded_files_on_clear()
{
return active_platform_services().deletes_recorded_files_on_clear();
}
void App::clear_platform_recorded_files(std::string path)
{
active_platform_services().clear_recorded_files(path);
}
void App::publish_exported_image(std::string path)
{
active_platform_services().publish_exported_image(path);
}
void App::flush_platform_storage()
{
active_platform_services().flush_persistent_storage();
}
std::vector<std::string> App::document_browse_roots() const
{
return active_platform_services().document_browse_roots(work_path, data_path);
}
void App::save_platform_ui_state()
{
active_platform_services().save_ui_state();
}
bool App::platform_enables_live_asset_reloading()
{
return active_platform_services().enables_live_asset_reloading();
}
void App::update_platform_frame(float delta_time_seconds)
{
active_platform_services().update_platform_frame(delta_time_seconds);
}
void App::report_rendered_frames(int frames)
{
active_platform_services().report_rendered_frames(frames);
}
void App::save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> callback)
{
active_platform_services().save_prepared_file(
path,
suggested_name,
[callback = std::move(callback)](std::string saved_path, bool saved) {
callback(saved_path, saved);
});
}
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
{
redraw = true;
const auto plan = pp::app::plan_app_pointer_dispatch(
x,
y,
zoom,
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Mouse down dispatch plan failed: %s", plan.status().message);
return false;
}
redraw = plan.value().request_redraw;
MouseEvent e;
e.m_type = button ? kEventType::MouseDownR : kEventType::MouseDownL;
e.m_pos = { x / zoom, y / zoom };
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_pressure = pressure;
e.m_source = source;
e.m_eraser = eraser;
kEventResult ret = kEventResult::Available;
if (auto* main = layout_designer[main_id])
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
return main->on_event(&e) == kEventResult::Consumed;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::mouse_move(float x, float y, float pressure, kEventSource source, bool eraser)
{
cursor = { x / zoom, y / zoom };
redraw = true;
const auto plan = pp::app::plan_app_pointer_dispatch(
x,
y,
zoom,
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Mouse move dispatch plan failed: %s", plan.status().message);
return false;
}
cursor = { plan.value().normalized_x, plan.value().normalized_y };
redraw = plan.value().request_redraw;
MouseEvent e;
e.m_type = kEventType::MouseMove;
e.m_pos = { x / zoom, y / zoom };
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_pressure = pressure;
e.m_source = source;
e.m_eraser = eraser;
kEventResult ret = kEventResult::Available;
if (auto* main = layout_designer[main_id])
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
return main->on_event(&e) == kEventResult::Consumed;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::mouse_up(int button, float x, float y, kEventSource source, bool eraser)
{
redraw = true;
const auto plan = pp::app::plan_app_pointer_dispatch(
x,
y,
zoom,
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Mouse up dispatch plan failed: %s", plan.status().message);
return false;
}
redraw = plan.value().request_redraw;
MouseEvent e;
e.m_type = button ? kEventType::MouseUpR : kEventType::MouseUpL;
e.m_pos = { x / zoom, y / zoom };
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_source = source;
e.m_eraser = eraser;
kEventResult ret = kEventResult::Available;
if (auto* main = layout_designer[main_id])
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
return main->on_event(&e) == kEventResult::Consumed;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::mouse_scroll(float x, float y, float delta)
{
redraw = true;
const auto plan = pp::app::plan_app_pointer_dispatch(
x,
y,
zoom,
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Mouse scroll dispatch plan failed: %s", plan.status().message);
return false;
}
redraw = plan.value().request_redraw;
MouseEvent e;
e.m_type = kEventType::MouseScroll;
e.m_pos = { x / zoom, y / zoom };
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_scroll_delta = delta;
kEventResult ret = kEventResult::Available;
if (auto* main = layout_designer[main_id])
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
return main->on_event(&e) == kEventResult::Consumed;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::mouse_cancel(int button)
{
redraw = true;
const auto plan = pp::app::plan_app_mouse_cancel_dispatch(
layout_designer.get(main_id) != nullptr,
layout.get(main_id) != nullptr);
redraw = plan.request_redraw;
MouseEvent e;
e.m_type = kEventType::MouseCancel;
kEventResult ret = kEventResult::Available;
if (auto* main = layout_designer[main_id])
if (auto* main = layout_designer[main_id]; plan.dispatch_designer_first && main)
return main->on_event(&e) == kEventResult::Consumed;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main_if_not_consumed && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::gesture_start(const glm::vec2& p0, const glm::vec2& p1)
{
redraw = true;
const auto plan = pp::app::plan_app_gesture_dispatch(
p0.x,
p0.y,
p1.x,
p1.y,
p0.x,
p0.y,
p1.x,
p1.y,
zoom,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Gesture start dispatch plan failed: %s", plan.status().message);
return false;
}
redraw = plan.value().request_redraw;
GestureEvent e;
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
e.m_type = kEventType::GestureStart;
e.m_pos = p / glm::vec2(zoom);
e.m_distance = glm::distance(p0, p1);
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_distance = plan.value().distance;
gesture_p0 = p0;
gesture_p1 = p1;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::gesture_move(const glm::vec2& p0, const glm::vec2& p1)
{
redraw = true;
const auto plan = pp::app::plan_app_gesture_dispatch(
p0.x,
p0.y,
p1.x,
p1.y,
gesture_p0.x,
gesture_p0.y,
gesture_p1.x,
gesture_p1.y,
zoom,
layout.get(main_id) != nullptr);
if (!plan) {
LOG("Gesture move dispatch plan failed: %s", plan.status().message);
return false;
}
redraw = plan.value().request_redraw;
GestureEvent e;
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
e.m_type = kEventType::GestureMove;
e.m_pos = p / glm::vec2(zoom);
e.m_distance = glm::distance(p0, p1);
e.m_distance_delta = e.m_distance - glm::distance(gesture_p0, gesture_p1);
e.m_pos_delta = p - glm::lerp(gesture_p0, gesture_p1, 0.5f);
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
e.m_distance = plan.value().distance;
e.m_distance_delta = plan.value().distance_delta;
e.m_pos_delta = { plan.value().position_delta_x, plan.value().position_delta_y };
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::gesture_end()
{
redraw = true;
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
redraw = plan.request_redraw;
GestureEvent e;
e.m_type = kEventType::GestureEnd;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::touch_tap(const glm::vec2& pos, int fingers, int tap_count)
{
redraw = true;
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
redraw = plan.request_redraw;
TouchEvent e;
e.m_type = kEventType::TouchTap;
e.m_finger_count = fingers;
e.m_tap_count = tap_count;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::key_down(kKey key)
{
if (key == kKey::KeySpacebar && vr_active)
const auto plan = pp::app::plan_app_key_down_dispatch(
layout.get(main_id) != nullptr,
key == kKey::KeySpacebar,
vr_active);
if (plan.sync_vr_camera_rotation)
canvas->m_canvas->m_cam_rot = vr_rot;
redraw = true;
keys[(int)key] = true;
redraw = plan.request_redraw;
keys[(int)key] = plan.set_key_down;
KeyEvent e;
e.m_type = kEventType::KeyDown;
e.m_key = key;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::key_up(kKey key)
{
redraw = true;
keys[(int)key] = false;
const auto plan = pp::app::plan_app_key_up_dispatch(layout.get(main_id) != nullptr);
redraw = plan.request_redraw;
keys[(int)key] = plan.set_key_down;
KeyEvent e;
e.m_type = kEventType::KeyUp;
e.m_key = key;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
bool App::key_char(char key)
{
redraw = true;
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
redraw = plan.request_redraw;
KeyEvent e;
e.m_type = kEventType::KeyChar;
e.m_char = key;
kEventResult ret = kEventResult::Available;
if (auto* main = layout[main_id])
if (auto* main = layout[main_id]; plan.dispatch_main && main)
ret = main->on_event(&e);
return ret == kEventResult::Consumed;
}
void App::toggle_ui()
{
auto m = layout[main_id]->m_children[1];
ui_visible = !ui_visible;
for (int i = 1; i < m->m_children.size(); i++)
m->m_children[i]->m_display = ui_visible;
auto* main = layout[main_id];
const std::size_t main_child_count = main ? main->m_children.size() : 0U;
auto* panel_container = main_child_count > 1U ? main->m_children[1].get() : nullptr;
const auto plan = pp::app::plan_app_ui_visibility_toggle(
ui_visible,
main != nullptr,
main_child_count,
panel_container ? panel_container->m_children.size() : 0U);
if (!plan) {
LOG("UI toggle plan failed: %s", plan.status().message);
return;
}
ui_visible = plan.value().next_ui_visible;
if (!panel_container)
return;
for (std::size_t i = plan.value().first_panel_child_index;
i < plan.value().panel_child_count;
++i) {
panel_container->m_children[i]->m_display = ui_visible;
}
}
void App::set_stylus()
{
has_stylus = true;
if (canvas)
const auto plan = pp::app::plan_app_stylus_attach(canvas != nullptr);
has_stylus = plan.set_has_stylus;
if (plan.enable_canvas_touch_lock && canvas)
canvas->m_canvas->m_touch_lock = true;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +1,68 @@
#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 {
void apply_shader_manager_feature_state(pp::renderer::gl::OpenGlFeatureState feature_state) noexcept
{
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);
}
}
void App::initShaders()
{
#ifdef _DEBUG
if (!check_uniform_uniqueness())
std::logic_error("check_uniform_uniqueness() failed");
LOG("check_uniform_uniqueness() failed");
#endif // _DEBUG
render_task([] {
GLint n_exts;
glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
for (int i = 0; i < n_exts; i++)
{
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
if (ext.find("shader_framebuffer_fetch") != std::string::npos)
ShaderManager::ext_framebuffer_fetch = true;
if (ext.find("map_buffer_alignment") != std::string::npos)
ShaderManager::ext_map_aligned = true;
#if __GLES__ && !__WEB__
if (ext.find("texture_float") != std::string::npos)
ShaderManager::ext_float32 = true;
if (ext.find("texture_float_linear") != std::string::npos)
ShaderManager::ext_float32_linear = true;
if (ext.find("color_buffer_float") != std::string::npos)
ShaderManager::ext_float32 = true;
if (ext.find("texture_half_float") != std::string::npos)
ShaderManager::ext_float16 = true;
if (ext.find("color_buffer_half_float") != std::string::npos)
ShaderManager::ext_float16 = true;
#endif
LOG("EXT: %s", ext.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;
}
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
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");
LOG("initializing shaders");
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl"))
LOG("Failed to create shader Texture");
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl"))
LOG("Failed to create shader TextureAlpha");
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl"))
LOG("Failed to create shader TextureMask");
if (!ShaderManager::load(kShader::TextureColorize, "data/shaders/texture-colorize.glsl"))
LOG("Failed to create shader TextureColorize");
if (!ShaderManager::load(kShader::TextureBlend, "data/shaders/texture-blend.glsl"))
LOG("Failed to create shader TextureBlend");
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl"))
LOG("Failed to create shader StrokePreview");
if (!ShaderManager::load(kShader::CompErase, "data/shaders/comp-erase.glsl"))
LOG("Failed to create shader CompErase");
if (!ShaderManager::load(kShader::CompDraw, "data/shaders/comp-draw.glsl"))
LOG("Failed to create shader CompDraw");
if (!ShaderManager::load(kShader::Color, "data/shaders/color.glsl"))
LOG("Failed to create shader Color");
if (!ShaderManager::load(kShader::ColorQuad, "data/shaders/color-quad.glsl"))
LOG("Failed to create shader ColorQuad");
if (!ShaderManager::load(kShader::ColorTri, "data/shaders/color-tri.glsl"))
LOG("Failed to create shader ColorTri");
if (!ShaderManager::load(kShader::ColorHue, "data/shaders/color-hue.glsl"))
LOG("Failed to create shader ColorHue");
if (!ShaderManager::load(kShader::UVs, "data/shaders/uvs.glsl"))
LOG("Failed to create shader UVs");
if (!ShaderManager::load(kShader::Font, "data/shaders/font.glsl"))
LOG("Failed to create shader Font");
if (!ShaderManager::load(kShader::Atlas, "data/shaders/atlas.glsl"))
LOG("Failed to create shader Atlas");
if (!ShaderManager::load(kShader::Stroke, "data/shaders/stroke.glsl"))
LOG("Failed to create shader Stroke");
if (!ShaderManager::load(kShader::StrokePad, "data/shaders/stroke-pad.glsl"))
LOG("Failed to create shader StrokePad");
if (!ShaderManager::load(kShader::StrokeDilate, "data/shaders/stroke-dilate.glsl"))
LOG("Failed to create shader StrokeDilate");
if (!ShaderManager::load(kShader::Checkerboard, "data/shaders/checkerboard.glsl"))
LOG("Failed to create shader Checkerboard");
if (!ShaderManager::load(kShader::Equirect, "data/shaders/equirect.glsl"))
LOG("Failed to create shader Equirect");
if (!ShaderManager::load(kShader::BrushStroke, "data/shaders/stroke-instanced.glsl"))
LOG("Failed to create shader BrushStroke");
if (!ShaderManager::load(kShader::VertexColor, "data/shaders/vertex-color.glsl"))
LOG("Failed to create shader VertexColor");
if (!ShaderManager::load(kShader::Lambert, "data/shaders/lambert.glsl"))
LOG("Failed to create shader Lambert");
if (!ShaderManager::load(kShader::LambertLightmap, "data/shaders/lightmap.glsl"))
LOG("Failed to create shader LambertLightmap");
if (!ShaderManager::load(kShader::BakeUV, "data/shaders/bake-uv.glsl"))
LOG("Failed to create shader BakeUV");
const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
if (!catalog_status.ok())
{
LOG("Shader catalog validation failed: %s", catalog_status.message);
return;
}
for (const auto& shader : shader_catalog)
{
if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
LOG("Failed to create shader %s", shader.name);
}
LOG("shaders initialized");
}

View File

@@ -1,12 +1,88 @@
#include "pch.h"
#include <cstdint>
#include "app.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)
{
pp::legacy::ui_gl::activate_texture_unit(unit_index, "OpenGL VR");
}
void unbind_texture_2d()
{
pp::legacy::ui_gl::unbind_texture_2d("OpenGL VR");
}
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 = pp::legacy::ui_gl::set_opengl_viewport,
});
if (!status.ok())
LOG("OpenGL VR UI viewport failed: %s", status.message);
}
void apply_vr_ui_scissor_test(bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
enabled,
pp::renderer::gl::OpenGlScissorTestDispatch {
.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);
}
void apply_vr_render_capability(std::uint32_t state, bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_capability(
state,
enabled,
pp::renderer::gl::OpenGlCapabilityDispatch {
.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 = pp::legacy::ui_gl::clear_opengl_buffer,
});
if (!status.ok())
LOG("OpenGL VR depth clear failed: %s", status.message);
}
}
bool trigger_down = false;
cbuffer<glm::vec3> controller_points(10);
@@ -19,31 +95,28 @@ 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()
{
uirtt.bindFramebuffer();
uirtt.clear();
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
glEnable(GL_SCISSOR_TEST);
apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect {
.width = static_cast<std::int32_t>(uirtt.getWidth()),
.height = static_cast<std::int32_t>(uirtt.getHeight()),
});
apply_vr_ui_scissor_test(true);
auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_vr_ui_scissor_test(false);
uirtt.unbindFramebuffer();
}
@@ -185,12 +258,12 @@ 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(GL_BLEND);
auto depth = glIsEnabled(GL_DEPTH_TEST);
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());
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
clear_vr_depth_buffer();
for (int plane_index = 0; plane_index < 6; plane_index++)
{
@@ -205,9 +278,9 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
m_face_plane.draw_fill();
}
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), true);
clear_vr_depth_buffer();
for (size_t i = 0; i < canvas->m_canvas->m_layers.size(); i++)
{
@@ -241,17 +314,17 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
//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);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
m_face_plane.draw_fill();
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else if (canvas->m_canvas->m_show_tmp && canvas->m_canvas->m_current_layer_idx == layer_index)
@@ -292,28 +365,28 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
canvas->m_canvas->m_tmp_dual[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE4);
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
m_face_plane.draw_fill();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
canvas->m_canvas->m_tmp_dual[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else
@@ -325,7 +398,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
m_face_plane.draw_fill();
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
@@ -352,7 +425,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
m_face_plane.draw_stroke();
}
glDisable(GL_DEPTH_TEST);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
// draw the brush
/*
auto mode = dynamic_cast<CanvasModePen*>(canvas->m_canvas->modes[(int)canvas->m_canvas->m_current_mode][0]);
@@ -377,8 +450,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size / height)) *
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
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;
tex.bind();
sampler_linear.bind(0);
@@ -399,7 +472,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
uirtt.bindTexture();
m_face_plane.draw_fill();
uirtt.unbindTexture();
@@ -451,8 +524,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
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))
);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
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;
tex.bind();
sampler_linear.bind(0);
@@ -466,7 +539,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
for (auto& mode : *canvas->m_canvas->m_mode)
mode->on_Draw(ortho_proj, proj, camera);
glDisable(GL_DEPTH_TEST);
glDisable(pp::renderer::gl::depth_test_state());
if (canvas->m_canvas->m_smask_active)
{
canvas->m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
@@ -479,8 +552,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
mode->on_Draw(ortho_proj, proj, camera);
*/
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth);
sampler.unbind();
}

View File

@@ -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);

View File

@@ -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);

Some files were not shown because too many files have changed in this diff Show More