296 Commits

Author SHA1 Message Date
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
238 changed files with 56416 additions and 4331 deletions

2
.gitignore vendored
View File

@@ -53,3 +53,5 @@ linux/Makefile
webgl/build
webgl/.vscode
out/

566
CMakeLists.txt Normal file
View File

@@ -0,0 +1,566 @@
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)
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(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/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)
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_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)
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/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_preferences.h
src/app_core/app_status.h
src/app_core/brush_ui.h
src/app_core/canvas_tool_ui.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_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})
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_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_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_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_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_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()

295
CMakePresets.json Normal file
View File

@@ -0,0 +1,295 @@
{
"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",
"architecture": "x64"
},
{
"name": "windows-msvc-vcpkg-headless",
"inherits": "platform-headless-base",
"displayName": "Windows MSVC vcpkg headless",
"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

@@ -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,26 @@
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"
VERBATIM)
endfunction()

View File

@@ -0,0 +1,170 @@
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/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/platform_legacy/legacy_platform_services.cpp
src/platform_legacy/legacy_platform_services.h
src/version.cpp
)
set(PP_PANOPAINTER_UI_SOURCES
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.

View File

@@ -0,0 +1,584 @@
# Build And Platform Inventory
Status: live
Last updated: 2026-06-03
This inventory records the known build surfaces during the CMake migration.
Keep it updated as platform paths move to shared CMake targets.
## Existing Build Entrypoints
| Platform/Target | Current Entrypoint | Notes |
| --- | --- | --- |
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard/display/share/picker service contracts owned by `pp_platform_api`, and injected `WindowsPlatformServices` now isolated in `src/platform_windows/windows_platform_services.*`; retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` |
| Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today |
| Android standard | `android/android/build.gradle`, `android/android/CMakeLists.txt` | Native library target `native-lib` |
| Android Quest | `android/quest/build.gradle`, `android/quest/CMakeLists.txt` | OVR SDK imported libraries |
| Android Focus/Wave | `android/focus/build.gradle`, `android/focus/CMakeLists.txt` | Wave SDK imported libraries |
| Linux | `linux/CMakeLists.txt` | Old CMake 3.4, C++14 flag |
| WebGL/Emscripten | `webgl/CMakeLists.txt` | Old CMake 3.4, WebGL2 flags |
## Existing Version Generation
- Script: `scripts/pre-build.py`
- Output: `src/version.gen.h`
- Current behavior: derives version from git branch, latest tag, short hash,
commit count, and configuration argument.
- Migration requirement: root CMake should call this script through a custom
command and avoid unnecessary tracked-file churn where possible.
## Existing Dependency Sources
Hybrid policy: migrate reliable packages to vcpkg and retain SDK/patched
dependencies until each platform triplet is proven.
| Dependency | Current Source | Initial Policy |
| --- | --- | --- |
| fmt | `libs/fmt` | Move to vcpkg |
| GLM | `libs/glm` | Move to vcpkg |
| tinyxml2 | `libs/tinyxml2` | Move to vcpkg |
| stb | `libs/stb` | Move to vcpkg or interface target if package friction |
| CURL | `libs/curl-win`, `libs/curl-android-ios` | Move to vcpkg where triplets work |
| SQLite | `libs/sqlite3` | Move to vcpkg |
| GLAD | `libs/glad` | Move to vcpkg or generated backend target |
| Catch2 | none yet | Add through vcpkg |
| OpenVR | `libs/openvr` | Retain initially |
| OVR Platform/Mobile | `libs/ovr_platform`, `libs/ovr_mobile` | Retain initially |
| Wave SDK | `libs/wave_sdk` | Retain initially |
| Wacom WinTab | `libs/wacom` | Retain initially |
| AppCenter Apple | `libs/appcenter-apple` | Retain initially |
| openh264/mp4v2/libyuv | `libs/openh264`, `libs/mp4v2`, `libs/libyuv` | Retain initially |
| jpeg helpers | `libs/jpeg` | Evaluate after image tests exist |
| poly2tri/nanort/base64/hash-library | `libs/*` | Evaluate after component split |
## Current Validation Commands
These commands are the current local baseline.
```powershell
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
ctest --preset desktop-fast --build-config Debug
ctest --preset fuzz --build-config Debug
ctest --preset stress --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
powershell -ExecutionPolicy Bypass -File scripts\automation\analyze.ps1 -Preset windows-msvc-default -NoApp
$env:VCPKG_ROOT = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"
cmake --preset windows-msvc-vcpkg-headless
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets windows-msvc-vcpkg-headless
ctest --preset desktop-fast-vcpkg --build-config Debug
cmake --preset android-arm64
powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug
cmake --fresh --preset windows-clangcl-asan
```
Known local toolchain state:
- CMake: 4.0.0-rc4
- Local Visual Studio generator selected by CMake: Visual Studio 17 2022
- Bundled vcpkg: `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg`
(`vcpkg version` reports 2025-11-19)
- Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk`
- Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865`
- clang-cl: `C:\Program Files\LLVM\bin\clang-cl.exe` reports 18.1.8, but the
selected VS 2026-preview STL expects Clang 20 or newer; see DEBT-0014 before
treating `windows-clangcl-asan` as a passing sanitizer gate.
- Android arm64 headless configure/build passes through root CMake and the
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`,
`pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation binary-stream/event/logging/task queue coverage, PNG metadata and
decode, PPI header/layout/non-finite opacity and blend-mode rejection, settings document, document
snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage,
snapshot-embedded duplicate/invalid face-payload and selection-mask rejection, paint brush/final-blend/
stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage,
renderer shader descriptor and OpenGL capability coverage, UI
color parsing, and layout XML parse coverage.
- Root CMake exposes named `fuzz` and `stress` CTest presets. `fuzz` currently
runs deterministic parser/serializer edge tests for binary streams, image
metadata, PPI, stroke scripts, and layout XML; `stress` currently runs the
stroke sampler stress coverage.
- `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
- `pp_assets_image_pixels_tests` decodes PNG payloads, encodes RGBA8 pixels to
PNG, round-trips encoded pixels back through the decoder, and rejects corrupt
or malformed image payloads.
- `pano_cli import-image` accepts a PNG path, decodes RGBA8 pixels through
`pp_assets`, attaches them to a pure `pp_document` face payload, and is
covered for checked-in decodable PNG import by `pano_cli_import_image_smoke`
and metadata-valid truncated PNG rejection by
`pano_cli_import_image_rejects_truncated_png`.
- `pano_cli export-image` writes a deterministic RGBA8 PNG through `pp_assets`
and is covered by `pano_cli_export_image_roundtrip_smoke`, which imports the
generated file back through `pano_cli import-image`.
- `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
body summary fields, layer/frame descriptors, and dirty-face PNG payload
metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a
minimal PPI fixture.
- `pp_document_ppi_import_tests` attaches decoded PPI dirty-face payloads to
`pp_document` layer/frame storage and rejects payloads outside document
layers.
- `pp_document_ppi_export_tests` exports pure `pp_document` metadata,
per-layer frame durations, and RGBA8 face payloads to PPI bytes through
`pp_assets`, then decodes and reimports them for round-trip coverage.
- `pano_cli simulate-document-export` exposes the same pure document-to-PPI
export, asset-level decode, and document reimport path through JSON
automation and is covered by `pano_cli_simulate_document_export_smoke`.
- `pano_cli save-document-project` writes that pure document export to a PPI
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
which inspects and loads the generated file.
- `pano_cli load-project` creates a `pp_document` projection with per-layer
frame counts, durations, and decoded face-pixel payloads when present; the
metadata-only minimal fixture remains covered by
`pano_cli_load_project_metadata_smoke`.
- `pp_assets::create_ppi_project` writes generated multi-layer, multi-frame
PPI files with explicit per-layer names, opacity, blend mode, alpha lock,
visibility, per-layer frame durations, and targeted dirty-face layer/frame
payloads. `pano_cli save-project` exposes that path for automation and is
covered by `pano_cli_save_project_roundtrip_smoke` and
`pano_cli_save_project_payload_roundtrip_smoke`, which reload generated
metadata-only and targeted dirty-face-payload projects through
`pano_cli load-project`, plus
`pano_cli_save_project_rejects_non_finite_opacity`, which verifies rejected
automation floats do not create output files.
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
is covered by `pano_cli_create_animation_document_smoke`.
- `pano_cli simulate-document-edits` exercises pure document layer/frame edit
operations, renderer-free face payloads, and renderer-free selection masks,
and is covered by `pano_cli_simulate_document_edits_smoke`.
- `pano_cli simulate-document-history` exercises pure document history
apply/undo/redo behavior and is covered by
`pano_cli_simulate_document_history_smoke`.
- `pano_cli simulate-image-import` decodes an embedded tiny PNG through
`pp_assets`, attaches it to `pp_document`, and is covered by
`pano_cli_simulate_image_import_smoke`.
- `pano_cli simulate-blend` exposes deterministic final RGBA and stroke-alpha
blend reference vectors through JSON automation and is covered by
`pano_cli_simulate_blend_smoke`.
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
automation and is covered by `pano_cli_simulate_stroke_smoke`.
- `pano_cli simulate-stroke-script` loads a text stroke script fixture and is
covered by `pano_cli_simulate_stroke_script_smoke`.
- `pano_cli apply-stroke-script` parses a text stroke script fixture, samples
every stroke through `pp_paint`, maps the samples into a bounded
`pp_document` RGBA8 face payload, writes a PPI file, and is covered by
`pano_cli_apply_stroke_script_roundtrip_smoke`, which inspects the dirty-face
box and loads the generated file back as decoded document pixel data, plus
`pano_cli_apply_stroke_script_rejects_tiny_canvas` for invalid dimension
rejection.
- `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes.
- `pp_renderer_api` owns the canonical PanoPainter shader catalog consumed by
the legacy OpenGL app initialization path; `pp_renderer_api_tests` validates
catalog size, key entries, duplicate rejection, and bad path rejection.
- `pp_renderer_gl` owns headless OpenGL runtime capability detection consumed
by the legacy app initialization path; `pp_renderer_gl_capabilities_tests`
validates framebuffer fetch, map-buffer alignment, desktop GL float support,
GLES float/half-float extensions, WebGL exclusion behavior, and the
upload-type mapping used by legacy `Texture2D` and `RTT` creation, plus the
RGBA pixel-format mapping used by `RTT` texture allocation. It also validates
image channel-count to OpenGL texture format mapping, including
invalid channel counts rejected by `Texture2D::create(Image)`, renderer API
texture-format to OpenGL internal/pixel/component token mapping including
depth-stencil formats, RGBA8/RGBA32F
readback formats, checked byte-count math, and PBO pixel-buffer target/usage/access
mapping used by `RTT` and `PBO` readbacks, and framebuffer status naming
used by `RTT` and `Texture2D` diagnostics. It also owns the 2D texture target,
framebuffer setup, readback format, mipmap target, and update component-type
tokens used by `Texture2D`, plus cube-map binding and allocation face targets
used by `TextureCube`. It also owns and
validates framebuffer blit color mask and linear/nearest filters used by
`RTT::resize` and `RTT::copy`, renderer API blit-filter to OpenGL token
mapping, plus the default linear clamp-to-edge
render-target texture parameters, texture/renderbuffer targets, depth format,
framebuffer targets, binding queries, attachment points, and completion
status used by `RTT::create` and framebuffer bind/restore paths, plus RTT
clear color/depth masks, renderer API render-pass color/depth/stencil
clear-mask and clear-value mapping, and color-write-mask query tokens. `RTT` no longer
spells GL enum names directly. It also
validates renderer API primitive-topology to OpenGL draw-mode mapping, Shape
index-type, fill/stroke primitive-mode, buffer target, static upload usage,
and vertex attribute component/normalization mapping used by
the legacy mesh draw path. Legacy `Shape` mesh buffer/VAO creation, zero-byte
dynamic-buffer creation, dynamic vertex/index uploads, fill/stroke draw
calls, and buffer/VAO deletion now consume tested dispatch contracts here,
plus the PanoPainter cube-face to OpenGL
texture-target mapping used by `TextureCube`.
It also owns and validates sampler wrap S/T/R, min/mag filter, and desktop
border-color parameter mapping used by legacy `Sampler`, plus renderer API
sampler filter/address-mode to OpenGL token mapping including mirrored-repeat
and aggregate renderer API sampler-state to OpenGL min/mag/wrap mapping.
Legacy `TextureCube` allocation/bind/delete and legacy `Sampler`
create/configure/border/bind/unbind calls now consume those resource dispatch
contracts directly from the retained app utilities.
The PanoPainter
shader attribute binding catalog, shader stage tokens, compile/link status
queries, active-uniform count query, and matrix-uniform transpose token used
by legacy `Shader` creation also live here. Renderer API blend factor/op to
OpenGL token mapping is tested here with explicit support flags so `GL_ZERO`
stays distinguishable from unsupported enum values. Aggregate renderer API
blend-state to OpenGL enable/factor/equation/color-mask mapping, depth
compare-op to OpenGL depth-function mapping, and aggregate renderer API
depth-state to OpenGL enable/write/compare mapping are tested here too.
`Shader` no longer spells GL enum
names directly. It also owns the PanoPainter shader uniform catalog and legacy hash
mapping used by `Shader` active-uniform discovery and the uniform uniqueness
check. Legacy `Shader` program use/delete, uniform writes, and
attribute-location lookup now consume tested dispatch contracts here.
Legacy shader source compilation, shader deletion, program attach/link,
attribute rebinding, active-uniform count/enumeration, and uniform-location
discovery now consume tested dispatch contracts as well, leaving the retained
shader utility with thin GL adapter functions.
App OpenGL initialization debug severity, debug output, GL info string,
renderer API viewport/scissor rect conversion, default depth/program-point/
line-smooth state, blend factor/equation, and UI render-target RGBA8 format
tokens are cataloged and tested here too, including the legacy convert command
and resize path. App clear color-buffer masks, default framebuffer binding,
scissor state, and sampler filter/wrap tokens also consume the backend mapping.
OpenGL extension enumeration query tokens
used before runtime capability detection are cataloged here. Legacy font
atlas texture formats, text mesh buffer targets, attribute component and
normalization tokens, draw primitive/index type, upload usage, and active
texture unit selection also consume the backend mapping. Text mesh
buffer/VAO creation, deferred index/vertex uploads, and indexed draw calls
now consume tested `pp_renderer_gl` mesh dispatch contracts too. Canvas undo/redo
dirty-region texture updates and readbacks also consume the backend-owned 2D
texture target, RGBA pixel format, and unsigned-byte component mapping.
`NodeViewport` preview rendering also consumes backend-owned viewport query,
clear-color query, color-buffer clear mask, and blend-state tokens.
`NodeImageTexture` preview drawing also consumes backend-owned fallback 2D
texture bind and blend-state tokens.
`NodeImage` drawing and remote-image texture creation also consume
backend-owned mipmapped sampler filters, blend-state tokens, and RGBA8/RGBA
texture format mapping.
`NodeColorWheel` triangle-buffer setup and draw-state handling also consume
backend-owned array-buffer, static-upload, vertex-attribute, primitive-mode,
and blend-state tokens.
Simple UI text, text-input, border, scroll, and animation timeline draw
paths also consume backend-owned blend-state tokens.
Canvas layer cube/equirect generation, clear, restore, and snapshot paths
also consume backend-owned cube/2D texture targets, active texture units,
blend/clear state, and RGBA8 read/write pixel mapping.
`NodePanelGrid` heightmap preview and lightmap baking also consume
backend-owned texture readback formats, sampler filters, depth/blend state,
depth clears, viewport queries, color-mask booleans, active texture units,
and float render-target formats.
Legacy `util.cpp` OpenGL error naming and `gl_state` save/restore also
consume backend-owned error codes, state queries, framebuffer targets,
texture binding targets, and active texture units.
`NodeStrokePreview` brush preview rendering also consumes backend-owned
depth/scissor/blend state, viewport/clear-color queries, active texture
units, 2D texture targets, copy targets, and sampler filters/wraps.
Legacy `Texture2D`, `TextureManager`, `Sampler`, and `RTT` public headers no
longer expose raw OpenGL enum defaults; default texture formats, sampler
filters/wraps, and render-target formats resolve through backend-owned
overloads.
The Windows entrypoint also consumes backend-owned generic OpenGL
error-code/info-string tokens and WGL core-context/pixel-format attribute
catalogs.
The headless OpenGL command planner consumes `pp_renderer_api` recorded
commands and maps render-pass clear masks/values, viewport/scissor state,
blend/depth/sampler state, texture formats, primitive modes, draw counts, and
blit filters into GL-facing planned command data while rejecting unsupported
enum tokens before a real GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
uploads, mipmap generation, texture transitions, texture copies, texture
readbacks, frame captures, passthrough commands, trace commands, unsupported
commands, and render-pass ordering errors such as state changes outside a
pass, nested passes, texture I/O or blits inside a pass, and unclosed passes.
It also validates executable command dependencies, including
shader-before-uniform and shader-plus-mesh before draw within each render
pass, and rejects invalid texture/sampler bind slots in malformed recorded
streams. `pano_cli record-render` emits the OpenGL plan texture/sampler bind
counts so automation can assert backend interpretation without an OpenGL
context.
Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
depth clear masks, active texture units, and fallback 2D texture unbind
targets while retaining the existing VR SDK/platform bridge shape.
Canvas mode overlay, mask, and transform paths also consume backend-owned
blend/depth state, active texture units, 2D texture copy targets, and RGBA8
readback format tokens.
`NodeCanvas` panorama UI rendering also consumes backend-owned sampler
defaults, viewport/clear-state queries, blend/depth/scissor state, color
clear masks, active texture units, fallback 2D texture unbind targets, copy
targets, and RGBA8 render-target formats.
Canvas resource setup also consumes backend-owned stroke-buffer
RGBA8/RGBA16F/RGBA32F formats, flood-fill texture upload format/type,
brush/stencil/mix sampler filters and wraps, and image channel-count texture
formats for cube-strip imports. Clamp-to-border sampler wrap is now part of
the backend capability catalog and test coverage.
Early canvas draw helpers also consume backend-owned pick readback
format/type, stroke mixer depth/scissor/blend state, saved viewport and
clear-state queries, active texture units, fallback 2D texture unbind
targets, and stroke background copy targets.
Canvas stroke commit also consumes backend-owned saved viewport/clear/blend
state, history readback format/type, active texture units, fallback 2D
texture unbind targets, and layer compositing copy targets.
Canvas layer merge rendering and explicit layer-merge compositing also consume
backend-owned depth/blend state, active texture units, fallback 2D texture
unbind targets, and merge framebuffer copy targets.
Canvas equirectangular import drawing and depth export rendering also consume
backend-owned depth/blend state and active texture units.
Canvas thumbnail generation and object-drawing helpers also consume
backend-owned saved viewport/clear/blend state, active texture units,
readback format/type, framebuffer copy targets, and renderbuffer/depth
attachment parameters; `src/canvas.cpp` no longer contains raw `GL_*`
constants.
Windows desktop OpenGL context creation now consumes a tested
`windows_wgl_core_context_3_3_config()` catalog from `pp_renderer_gl` instead
of owning active WGL context/pixel-format attribute literals in `main.cpp`.
- `windows-msvc-vcpkg-headless` validates manifest install/configure/build/test
for the current headless component matrix; see DEBT-0007 for remaining app
and platform triplet migration.
- `scripts/automation/analyze.*` runs shader validation plus a
renderer-boundary guard that reports JSON and fails if active non-backend
source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed
legacy OpenGL implementation files.
- `pp_renderer_api` exposes a headless `RecordingRenderDevice` that reports
renderer feature flags and validates backend-owned resource creation,
explicit texture usage flags, command order,
render-pass color/depth/stencil clear intent, scissor state, depth state,
blend state, texture-slot binding, sampler-state binding, texture-upload byte
counts, texture mip-level counts, texture/mesh/shader resource debug labels, mipmap-generation commands,
texture-state transitions, shader-uniform writes, explicit draw descriptor ranges, texture-copy regions,
readback/frame-capture/blit descriptor validation, readback bounds, destination buffer sizes, and
render-target blit regions, records
render-pass-clear/scissor/depth/blend/shader-uniform/texture-bind/
sampler-bind/draw/upload/mipmap-generation/texture-transition/texture-copy/readback/
frame-capture/blit commands, draw mesh inputs, explicit draw ranges, and
records trace markers and scopes without a window or GL context. Recorder
`clear()` also resets active render-pass and trace-scope state so automation
can reuse the same recording device after an interrupted frame.
- `pano_cli record-render` exposes the recording renderer through JSON
automation, including backend feature flags, render-pass/depth-clear counts, scissor/depth/blend/
shader-uniform/texture-bind/sampler-bind/upload/mipmap-generation/texture-transition/texture-copy/readback/
frame-capture/blit command and byte totals, trace marker/scope counts,
labeled descriptor counts, backend resource creation counts, plus draw
descriptor vertex/index totals. When `pp_renderer_gl` is available, it also
emits an `openGlPlan` JSON object with the planned command count, support
status, render-pass/draw/shader-bind/uniform/texture-upload/mipmap/
transition/copy/readback/capture/passthrough/trace counts, unsupported
command count, render-pass order error count, dependency error count, and
unclosed-pass state. Its
`--exercise-clear` mode verifies
interrupted-frame recorder clear/reuse behavior and reports the result in
JSON, and is covered by `pano_cli_record_render_smoke`,
`pano_cli_record_render_exercises_clear_reset`, plus
`pano_cli_record_render_rejects_oversized_target`.
- `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory`
apply/undo/redo state through JSON automation and is covered by
`pano_cli_simulate_document_history_smoke`.
- `pano_cli simulate-document-edits` exposes `pp_document` layer metadata,
frame order, active-index, tiny face-payload state, and selection-mask state
through JSON automation and is covered by
`pano_cli_simulate_document_edits_smoke`.
- `pano_cli simulate-image-import` exposes embedded PNG decode and document
face-payload attachment through JSON automation and is covered by
`pano_cli_simulate_image_import_smoke`.
- `pano_cli import-image` exposes file-driven PNG decode and document
face-payload attachment through JSON automation and is covered by
`pano_cli_import_image_smoke` and
`pano_cli_import_image_rejects_truncated_png`.
- `pano_cli export-image` exposes deterministic RGBA8 PNG writing through JSON
automation and is covered by `pano_cli_export_image_roundtrip_smoke`; full
legacy canvas export remains a future CLI task.
- `pano_cli save-project` exposes generated multi-layer, multi-frame PPI
writing with layer metadata and targeted dirty-face layer/frame payloads
through JSON automation and is covered by metadata-only and
dirty-face-payload round-trip smoke tests; full legacy canvas save parity
remains tracked by DEBT-0013.
- `pp_document::export_ppi_project_document` exposes pure document-to-PPI byte
export through CTest coverage; legacy Canvas save integration remains a
future DEBT-0010/DEBT-0013 task.
- `pano_cli simulate-document-export` exposes document export round-trip state
through JSON automation for agent-driven checks.
- `pano_cli save-document-project` exposes file-writing document export
automation for inspect/load round trips.
- `pano_cli apply-stroke-script` exposes file-driven stroke-script application
to a pure document face payload and writes a PPI artifact for inspect/load
round-trip automation.
- `pano_cli classify-open` exposes the `pp_app_core` document-open route
contract as JSON and is covered for project files, ABR imports, PPBR
imports, and malformed path rejection.
- `pano_cli plan-open-route` exposes `pp_app_core` document-open action
planning as JSON and is covered for clean project open, dirty project
discard-prompt, and ABR import-prompt states.
- `pano_cli plan-new-document` exposes `pp_app_core` new-document target,
legacy resolution-index mapping, and overwrite-prompt planning as JSON and is
covered for save-now, existing-target overwrite, and invalid-resolution
states.
- `pano_cli plan-document-file` exposes `pp_app_core` document-name
validation, legacy `.ppi` path construction, and overwrite-prompt decisions
as JSON through the same combined save-file plan consumed by the live save-as
dialog; it is covered for save-now and existing-target overwrite states.
- `pano_cli plan-document-version` exposes `pp_app_core` save-version suffix
parsing, candidate path generation, collision skipping, and no-slot failure
behavior as JSON and is covered for first-version and existing-path skip
states.
- `pano_cli plan-export-target` exposes `pp_app_core` export target planning
for image file exports, layer/frame collection directories, picked-directory
stems, and MP4 suggested names as JSON and is covered for file, collection,
and suggested-name states.
- `pano_cli plan-export-start` exposes `pp_app_core` export availability
planning for license-gated, demo-blocked, and missing-canvas states as JSON;
the live image, layer, animation-frame, depth, and cube-face export dialogs
plus MP4 animation and timelapse export dialogs consume the same start
contract before reaching legacy canvas/recording export execution.
- `pano_cli plan-recording-session` exposes `pp_app_core` recording start,
stop, clear, platform cleanup, frame-count reset, and export progress-total
planning as JSON; the live recording controls consume those contracts before
reaching legacy recording threads, PBO readback, and MP4 encoder execution.
- `pano_cli plan-share-file` exposes `pp_app_core` share availability planning
as JSON for unsaved and saved document paths; the live platform share command
consumes the same contract before reaching iOS/macOS sharing bridges or
retained no-op platform branches.
- `pano_cli plan-picked-path` exposes `pp_app_core` selected-path planning as
JSON for empty and non-empty file picker results; live image/file/save/
directory picker branches consume the same contract before invoking retained
platform callbacks or legacy picker bridges.
- `pano_cli plan-display-file` exposes `pp_app_core` external file presentation
planning as JSON for empty and non-empty paths; the live display-file command
consumes the same contract before retained platform open-file bridges.
- `pano_cli plan-keyboard-visibility` exposes `pp_app_core` virtual keyboard
visibility planning as JSON for hidden and visible states; live show/hide
keyboard requests consume the same contract before retained mobile platform
keyboard bridges.
- `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility
planning as JSON for hidden and visible states; live canvas cursor requests
consume the same contract before retained desktop platform cursor bridges.
- `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose
`pp_app_core` clipboard text planning as JSON, including empty text writes;
live clipboard get/set requests consume the same contracts before retained
platform clipboard bridges.
- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for
startup storage path preparation, clipboard text, cursor visibility,
virtual-keyboard visibility, external file display, file sharing, native
app/window close, UI-thread lifecycle hooks, render-context lifecycle hooks,
render-target binding hooks, render platform hint hooks, render-capture frame
hooks, render debug callback hooks, per-frame platform hooks, picker
callbacks, and recording cleanup, live asset/layout reload policy, diagnostic
stacktrace/crash hooks, prepared-file save/download handoff;
Windows
live app execution now uses injected
`WindowsPlatformServices` from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`,
while non-Windows platforms still reach retained platform bridges through
the debt-tracked adapter isolated in
`src/platform_legacy/legacy_platform_services.*`.
- `pp_renderer_gl` owns the tested `OpenGlInitialState` startup depth/blend
policy and dispatch application consumed by `App::init`, tested runtime
version/vendor/renderer/GLSL string query dispatch, tested default clear
color/buffer dispatch consumed by `App::clear`, tested app UI
viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`,
tested generic capability/buffer-clear dispatch consumed by VR draw state
setup, tested saved-state snapshot/restore dispatch consumed by the retained
`gl_state` utility, tested texture lifecycle/readback dispatch consumed by
the retained `Texture2D` utility, tested framebuffer blit/readback dispatch
consumed by retained `RTT` resize/copy/readback paths, tested framebuffer
bind/restore dispatch consumed by retained `RTT` render-target pass entry
and exit paths, plus renderer API to OpenGL token mapping and command-planning
contracts used by the OpenGL parity work.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before
reaching legacy UI, canvas save, and network upload execution.
- `pano_cli plan-cloud-upload-all` exposes bulk cloud upload file-count,
progress UI availability, and progress-total clamping as JSON; the live
upload-all command consumes the same contract before reaching legacy asset
file listing, OpenGL context guard, progress UI, and network upload
execution.
- `pano_cli plan-cloud-browse` exposes `pp_app_core` cloud browse availability
and selected-file download planning as JSON; the live cloud browse command
consumes those contracts before reaching legacy dialog, network download,
canvas project-open, layer UI, and action-history execution.
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
app-close, save, save-as, save-version, and save-before-workflow decisions
as JSON and is covered for clean, dirty, already-prompting, missing-canvas,
new-document, save-as, save-version, and dirty-save-version states.
- `pp_app_core_document_route_tests` covers the app document-open route
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or
legacy canvas work.
- `pp_app_core_document_export_tests` covers export file targets, collection
directory/stem targets, picked-directory stems, MP4 suggested names, and
invalid export naming inputs, plus export-start license/canvas availability
decisions.
- `pp_app_core_document_recording_tests` covers recording start/stop, clear,
platform recorded-file cleanup, frame-count reset, export progress totals,
and oversized progress-total clamping.
- `pp_app_core_document_sharing_tests` covers saved-path gating before platform
share execution.
- `pp_app_core_document_platform_io_tests` covers empty selected-path filtering
and non-empty picked-path callback planning, plus empty/non-empty display-file
planning before platform picker/display callbacks, plus virtual keyboard
show/hide planning before platform keyboard callbacks, plus cursor visibility
planning before platform cursor callbacks, plus clipboard read/write
planning before platform clipboard callbacks.
- `pp_platform_api_tests` covers service dispatch for clipboard read/write,
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
external file display, file sharing, and picker callbacks without platform
SDK headers or a window.
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download
decisions, plus bulk upload progress visibility, zero-file, and clamped
progress-total decisions.
- `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning, save-request, save-before-workflow,
new-document target/resolution/overwrite planning, document file target,
combined save-file overwrite planning, and save-version target decisions
without requiring a window, canvas, or message box.
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
through the vcpkg preset; default and Android validation still use the
retained vendored fallback tracked by DEBT-0012.
Known warnings after the current CMake app build:
- Legacy code/vendor warnings under `/W4`.
- `pp_legacy_vendor` intentionally owns retained third-party source builds for
now, including JPEG, SQLite, Yoga, poly2tri, GLAD, fmt, Wacom utilities, and
other patched/embedded sources. Each dependency should either move to vcpkg,
an SDK import target, or a documented permanent vendored target.
- `pp_legacy_assets_io` is an object-library containment boundary for retained
ABR, asset/file, binary stream, image, serializer, and settings code. It
should shrink as app I/O consumes `pp_assets` directly.
- `pp_legacy_paint_document` is an object-library containment boundary for
retained action, bezier, brush, canvas, canvas-layer, and event code. It
should shrink as app painting and document behavior consume `pp_paint` and
`pp_document` directly.
- `pp_legacy_engine` intentionally contains retained legacy runtime shell
sources for now, so it concentrates existing legacy tablet, video, HMD, log,
and low-level utility warnings until those paths move to cleaner component
ownership.
- `pp_legacy_renderer_gl` is an object-library containment boundary because
the retained OpenGL runtime classes still include legacy app/engine headers
and are still consumed directly by canvas and UI classes. It should become a
normal backend library once those call sites depend on `pp_renderer_api`.
- `pp_legacy_ui_core` is an object-library containment boundary because the
retained base `Node` controls still depend on legacy renderer and app
headers. It should shrink as layout parsing, colors, generic controls, and
text/image primitives move to `pp_ui_core`.
- `pp_panopainter_ui` currently surfaces existing legacy `Node`/`Serializer`
header and static-analysis warnings while it still depends on
`pp_legacy_app`; these should be reduced as the UI core/app UI boundary is
tightened instead of suppressed globally.
- `pp_app_core` is the first pure app-engine target consumed by
`panopainter_app`; it should grow only with UI-free command routing,
validation, and app service contracts that can be tested without a window.
- `panopainter_app` currently surfaces existing app orchestration, GLM,
base64, VR, and serializer warnings now that app sources live in the
composition target; warning cleanup should follow component ownership rather
than be hidden with target-wide suppressions.
- Visual Studio vcpkg manifest warning because manifest mode is not enabled.
- `LNK4099` missing `yuv.pdb` for retained libyuv binaries.
- `LNK4098` runtime library conflict from retained vendor binaries.
Platform-specific commands should be added here when verified locally.

View File

@@ -0,0 +1,84 @@
# PanoPainter Capability Map
Status: live
Last updated: 2026-06-03
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 |
| 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 |
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
## Image And Export
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests |
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests |
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
## 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, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke-feedback destination-copy coverage, and GPU parity |
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Layer add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document` | Undo/redo invariant tests |
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
| 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 and layout tests |
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts |
| 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 |
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | 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 |

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

@@ -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,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,249 @@
[CmdletBinding()]
param(
[string]$Preset = "windows-msvc-default",
[string]$Configuration = "Debug",
[string]$Target = "PanoPainter",
[string[]]$PackageKinds = @(
"windows-appx",
"android-standard-apk",
"android-quest-apk",
"android-focus-apk",
"apple-bundle",
"webgl"
)
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$root = (Get-Location).Path
function Test-CommandAvailable {
param([string]$Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
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-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 "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 "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 "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")
)
}
"webgl" {
$webDir = Join-Path $root "out/package/webgl"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "emscripten-preset-exists-but-webgl-package-target-missing" `
-ValidationCommand "cmake --build --preset emscripten --target PanoPainter" `
-Prerequisites @(
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
)
}
}
}
return $readiness
}
& cmake --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"
exitCode = $buildExitCode
elapsedMs = $elapsed
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
} | ConvertTo-Json -Compress
exit $buildExitCode
}
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
$dataDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "data"
$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 }
)
$failed = @($checks | Where-Object { -not $_.exists })
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "package-smoke"
preset = $Preset
configuration = $Configuration
target = $Target
exitCode = $exitCode
elapsedMs = $elapsedMs
checks = $checks
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
} | ConvertTo-Json -Compress -Depth 5
exit $exitCode

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env sh
set -u
preset="${1:-linux-clang}"
configuration="${2:-Debug}"
target="${3:-PanoPainter}"
artifact="${4:-out/build/$preset/$target}"
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"
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)"
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":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
printf ',{"kind":"webgl","status":"blocked","reason":"emscripten-preset-exists-but-webgl-package-target-missing","debt":"DEBT-0011","validationCommand":"cmake --build --preset emscripten --target PanoPainter","prerequisites":[{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
printf ']'
}
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,99 @@
[CmdletBinding()]
param(
[string[]]$Presets = @("android-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_image_format_tests",
"pp_assets_image_metadata_tests",
"pp_assets_image_pixels_tests",
"pp_assets_ppi_header_tests",
"pp_assets_settings_document_tests",
"pp_paint_brush_tests",
"pp_paint_blend_tests",
"pp_paint_stroke_tests",
"pp_paint_stroke_script_tests",
"pp_document_tests",
"pp_document_ppi_import_tests",
"pp_document_ppi_export_tests",
"pp_renderer_api_tests",
"pp_renderer_gl_capabilities_tests",
"pp_renderer_gl_command_plan_tests",
"pp_paint_renderer_compositor_tests",
"pp_platform_api_tests",
"pp_ui_core_color_tests",
"pp_ui_core_layout_value_tests",
"pp_ui_core_layout_xml_tests",
"pp_app_core_document_route_tests",
"pp_app_core_document_export_tests",
"pp_app_core_document_cloud_tests",
"pp_app_core_document_platform_io_tests",
"pp_app_core_document_recording_tests",
"pp_app_core_app_preferences_tests",
"pp_app_core_app_status_tests",
"pp_app_core_document_sharing_tests",
"pp_app_core_document_session_tests"
)
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$results = @()
$overallExitCode = 0
foreach ($preset in $Presets) {
& cmake --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)
}
& cmake @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
results = $results
} | ConvertTo-Json -Compress -Depth 6
exit $overallExitCode

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env sh
set -u
preset="${1:-android-arm64}"
shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_document_route_tests pp_app_core_document_export_tests pp_app_core_document_cloud_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_app_preferences_tests pp_app_core_app_status_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests}"
start="$(date +%s)"
cmake --preset "$preset"
configure_exit="$?"
if [ "$configure_exit" -ne 0 ]; then
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms"
exit "$configure_exit"
fi
build_args=""
for target in $targets; do
build_args="$build_args --target $target"
done
# shellcheck disable=SC2086
cmake --build --preset "$preset" $build_args
build_exit="$?"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms"
exit "$build_exit"

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

@@ -5,6 +5,13 @@
#include "node_dialog_open.h"
#include "node_progress_bar.h"
#include "mp4enc.h"
#include "app_core/app_status.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_recording.h"
#include "app_core/document_route.h"
#include "app_core/document_session.h"
#include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef __APPLE__
#include <Foundation/Foundation.h>
@@ -12,32 +19,156 @@
#endif
#include "settings.h"
#ifdef __ANDROID__
void android_async_lock();
void android_async_swap();
void android_async_unlock();
void android_attach_jni();
void android_detach_jni();
#elif _WIN32
bool async_lock_try();
void async_lock();
void win32_async_swap();
void async_unlock();
void destroy_window();
void win32_renderdoc_frame_start();
void win32_renderdoc_frame_end();
#elif __LINUX__
std::string linux_home_path();
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE);
#elif __WEB__
#include <unistd.h>
#endif
App* App::I = nullptr; // singleton
std::deque<AppTask> App::render_tasklist;
std::mutex App::render_task_mutex;
std::condition_variable App::render_cv;
namespace {
[[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept
{
return reinterpret_cast<const char*>(glGetString(static_cast<GLenum>(name)));
}
void enable_opengl_state(std::uint32_t state) noexcept
{
glEnable(static_cast<GLenum>(state));
}
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
{
switch (mode) {
case kCanvasMode::Draw:
return pp::app::CanvasToolMode::draw;
case kCanvasMode::Erase:
return pp::app::CanvasToolMode::erase;
case kCanvasMode::Line:
return pp::app::CanvasToolMode::line;
case kCanvasMode::Camera:
return pp::app::CanvasToolMode::camera;
case kCanvasMode::Grid:
return pp::app::CanvasToolMode::grid;
case kCanvasMode::Copy:
return pp::app::CanvasToolMode::copy;
case kCanvasMode::Cut:
return pp::app::CanvasToolMode::cut;
case kCanvasMode::Fill:
return pp::app::CanvasToolMode::fill;
case kCanvasMode::MaskFree:
return pp::app::CanvasToolMode::mask_free;
case kCanvasMode::MaskLine:
return pp::app::CanvasToolMode::mask_line;
case kCanvasMode::FloodFill:
return pp::app::CanvasToolMode::flood_fill;
default:
return pp::app::CanvasToolMode::draw;
}
}
void disable_opengl_state(std::uint32_t state) noexcept
{
glDisable(static_cast<GLenum>(state));
}
void set_opengl_blend_func(std::uint32_t source_factor, std::uint32_t destination_factor) noexcept
{
glBlendFunc(static_cast<GLenum>(source_factor), static_cast<GLenum>(destination_factor));
}
void set_opengl_blend_equation_separate(std::uint32_t color_equation, std::uint32_t alpha_equation) noexcept
{
glBlendEquationSeparate(static_cast<GLenum>(color_equation), static_cast<GLenum>(alpha_equation));
}
void clear_opengl_color(float r, float g, float b, float a) noexcept
{
glClearColor(r, g, b, a);
}
void clear_opengl_buffers(std::uint32_t mask) noexcept
{
glClear(static_cast<GLbitfield>(mask));
}
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glScissor(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
const auto status = pp::renderer::gl::apply_opengl_viewport(
viewport,
pp::renderer::gl::OpenGlViewportDispatch {
.viewport = set_opengl_viewport,
});
if (!status.ok())
LOG("OpenGL viewport failed: %s", status.message);
}
void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
scissor,
pp::renderer::gl::OpenGlScissorDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
.scissor = set_opengl_scissor,
});
if (!status.ok())
LOG("OpenGL scissor failed: %s", status.message);
}
void apply_app_scissor_test(bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
enabled,
pp::renderer::gl::OpenGlScissorTestDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
});
if (!status.ok())
LOG("OpenGL scissor test failed: %s", status.message);
}
[[nodiscard]] GLint rgba8_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
[[nodiscard]] GLenum linear_texture_filter() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::linear_texture_filter());
}
[[nodiscard]] GLenum nearest_texture_filter() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::nearest_texture_filter());
}
[[nodiscard]] GLenum repeat_texture_wrap() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::repeat_texture_wrap());
}
[[nodiscard]] GLenum framebuffer_target() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
}
[[nodiscard]] GLuint default_framebuffer_id() noexcept
{
return static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id());
}
}
std::thread App::render_thread;
std::thread::id App::render_thread_id;
bool App::render_running = false;
@@ -57,15 +188,14 @@ void App::create()
void App::open_document(std::string path)
{
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
std::smatch m;
if (!std::regex_search(path, m, r))
const auto route = pp::app::classify_document_open_path(path);
if (!route)
return;
std::string base = m[1].str();
std::string name = m[2].str();
std::string ext = m[3].str();
if (str_iequals(ext, "abr"))
const bool has_unsaved_project =
route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved;
const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project);
if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr)
{
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) {
@@ -73,7 +203,7 @@ void App::open_document(std::string path)
target->destroy();
};
}
else if (str_iequals(ext, "ppbr"))
else if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_ppbr)
{
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) {
@@ -83,6 +213,8 @@ void App::open_document(std::string path)
}
else
{
const std::string base = route.value().directory;
const std::string name = route.value().name;
auto open_action = [this, path, base, name] {
doc_name = name;
doc_dir = base;
@@ -109,7 +241,7 @@ void App::open_document(std::string path)
});
ActionManager::clear();
};
if (!Canvas::I->m_unsaved)
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
{
open_action();
}
@@ -127,25 +259,19 @@ void App::open_document(std::string path)
bool App::request_close()
{
static bool dialog_already_opened = false;
if (!Canvas::I->m_unsaved)
const auto close_decision = pp::app::plan_close_request(
Canvas::I->m_unsaved,
dialog_already_opened);
if (close_decision == pp::app::CloseRequestDecision::close_now)
return true;
if (!dialog_already_opened)
if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt)
{
auto* m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Do you want to close without saving?");
m->btn_ok->m_text->set_text("Yes");
m->btn_ok->on_click = [this](Node*) {
#ifdef _WIN32
destroy_window();
//PostQuitMessage(0);
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[osx_view close];
});
#elif __LINUX__
glfwSetWindowShouldClose(glfw_window, GLFW_TRUE);
#endif
request_app_close();
Canvas::I->m_unsaved = false;
};
m->btn_cancel->m_text->set_text("No");
@@ -160,8 +286,13 @@ bool App::request_close()
void App::clear()
{
glClearColor(.1f, .1f, .1f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
const auto status = pp::renderer::gl::clear_panopainter_default_target(
pp::renderer::gl::OpenGlClearDispatch {
.clear_color = clear_opengl_color,
.clear = clear_opengl_buffers,
});
if (!status.ok())
LOG("OpenGL clear failed: %s", status.message);
}
void App::initAssets()
@@ -170,9 +301,9 @@ void App::initAssets()
FontManager::init();
LOG("initializing assets create sampler");
sampler.create(GL_NEAREST);
sampler_stencil.create(GL_LINEAR, GL_REPEAT);
sampler_linear.create(GL_LINEAR);
sampler.create(nearest_texture_filter());
sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap());
sampler_linear.create(linear_texture_filter());
m_face_plane.create<1>(2, 2);
sphere.create<8, 8>(1);
LOG("initializing assets load uvs texture");
@@ -181,78 +312,16 @@ void App::initAssets()
void App::initLog()
{
#if defined(__IOS__)
[ios_view init_dirs];
#elif defined(__OSX__)
[osx_app init_dirs];
#elif defined(_WIN32)
//CHAR my_documents[MAX_PATH];
//HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
//HMODULE hModule = GetModuleHandle(NULL);
//CHAR path[MAX_PATH];
//GetModuleFileNameA(hModule, path, MAX_PATH);
//CHAR out_drive[MAX_PATH];
//CHAR out_path[MAX_PATH];
//_splitpath(path, out_drive, out_path, nullptr, nullptr);
//sprintf_s(path, "%s%s", out_drive, out_path);
//data_path = path;
CHAR my_documents[MAX_PATH];
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
if (SUCCEEDED(result))
{
std::string path = std::string(my_documents) + "\\PanoPainter";
if (!PathFileExistsA(path.c_str()))
CreateDirectoryA(path.c_str(), NULL);
data_path = path;
}
else
{
CHAR path[MAX_PATH];
GetCurrentDirectoryA(sizeof(path), path);
data_path = path;
}
rec_path = data_path + "\\frames";
if (!PathFileExistsA(rec_path.c_str()))
CreateDirectoryA(rec_path.c_str(), NULL);
if (!PathFileExistsA((data_path + "\\brushes").c_str()))
CreateDirectoryA((data_path + "\\brushes").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\brushes\\thumbs").c_str()))
CreateDirectoryA((data_path + "\\brushes\\thumbs").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\patterns").c_str()))
CreateDirectoryA((data_path + "\\patterns").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\patterns\\thumbs").c_str()))
CreateDirectoryA((data_path + "\\patterns\\thumbs").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\settings").c_str()))
CreateDirectoryA((data_path + "\\settings").c_str(), NULL);
#elif __LINUX__
data_path = linux_home_path() + "/PanoPainter";
mkpath(data_path + "/brushes");
mkpath(data_path + "/brushes/thumbs");
mkpath(data_path + "/patterns");
mkpath(data_path + "/patterns/thumbs");
mkpath(data_path + "/settings");
mkpath(data_path + "/frames");
#elif __WEB__
data_path = "/PanoPainter";
mkdir(data_path.c_str(), 0777);
mkdir((data_path + "/brushes").c_str(), 0777);
mkdir((data_path + "/brushes/thumbs").c_str(), 0777);
mkdir((data_path + "/patterns").c_str(), 0777);
mkdir((data_path + "/patterns/thumbs").c_str(), 0777);
mkdir((data_path + "/settings").c_str(), 0777);
mkdir((data_path + "/frames").c_str(), 0777);
#endif
const auto paths = prepare_storage_paths();
if (!paths.data_path.empty())
data_path = paths.data_path;
if (!paths.recording_path.empty())
rec_path = paths.recording_path;
if (!paths.temporary_path.empty())
tmp_path = paths.temporary_path;
// TODO: save this path somewhere in the settings, don't overwrite every start
work_path = data_path;
work_path = paths.work_path.empty() ? data_path : paths.work_path;
//LogRemote::I.start();
LogRemote::I.file_init();
@@ -385,56 +454,29 @@ void App::upload(std::string filename, std::string name, std::function<void(floa
#endif //CURL
}
#ifdef _WIN32
static CONSOLE_SCREEN_BUFFER_INFO info;
void handle_gl_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
{
static std::map<GLenum, int> colors = {
{ GL_DEBUG_SEVERITY_NOTIFICATION, 8 },
{ GL_DEBUG_SEVERITY_LOW, 8 },
{ GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY },
{ GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY },
};
if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
LOG("OPENGL: %.*s", length, message);
FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes);
#ifdef _DEBUG
if (severity == GL_DEBUG_SEVERITY_HIGH)
__debugbreak();
#endif
}
}
#endif
void App::init()
{
#ifdef _WIN32
if (glDebugMessageCallback)
{
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
render_task([]
{
glDebugMessageCallback(handle_gl_callback, nullptr);
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
});
}
#endif
LOG("Screen Resolution: %dx%d", (int)width, (int)height);
render_task([]
{
LOG("GL version: %s", glGetString(GL_VERSION));
LOG("GL vendor: %s", glGetString(GL_VENDOR));
LOG("GL renderer: %s", glGetString(GL_RENDERER));
LOG("GLSL version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
App::I->install_render_debug_callback();
const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
pp::renderer::gl::OpenGlRuntimeInfoDispatch {
.get_string = query_opengl_string,
});
if (runtime_info_result.ok())
{
const auto& runtime_info = runtime_info_result.value();
LOG("GL version: %s", runtime_info.version);
LOG("GL vendor: %s", runtime_info.vendor);
LOG("GL renderer: %s", runtime_info.renderer);
LOG("GLSL version: %s", runtime_info.shading_language_version);
}
else
{
LOG("OpenGL runtime info failed: %s", runtime_info_result.status().message);
}
//GLint n_exts;
//glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
@@ -447,13 +489,16 @@ void App::init()
// }
//}
glDisable(GL_DEPTH_TEST);
#if defined(_WIN32) || defined(__OSX__)
glEnable(GL_PROGRAM_POINT_SIZE);
glEnable(GL_LINE_SMOOTH);
#endif
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);
App::I->apply_render_platform_hints();
const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state(
pp::renderer::gl::OpenGlStateDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
.blend_func = set_opengl_blend_func,
.blend_equation_separate = set_opengl_blend_equation_separate,
});
if (!startup_state_status.ok())
LOG("OpenGL startup state failed: %s", startup_state_status.message);
});
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1;
@@ -468,7 +513,7 @@ void App::init()
initLayout();
title_update();
uirtt.create(width, height, -1, GL_RGBA8, true);
uirtt.create(width, height, -1, rgba8_internal_format(), true);
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true))
rec_start();
@@ -482,18 +527,7 @@ void App::init()
void App::async_start()
{
#if __OSX__
[osx_view async_lock];
#elif __IOS__
[ios_view async_lock];
#elif __ANDROID__
android_async_lock();
#elif _WIN32
async_lock();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#elif __LINUX__ || __WEB__
glfwMakeContextCurrent(glfw_window);
#endif
acquire_render_context();
}
void App::async_redraw()
@@ -504,30 +538,12 @@ void App::async_redraw()
void App::async_end()
{
#if __OSX__
[osx_view async_unlock];
#elif __IOS__
[ios_view async_unlock];
#elif __ANDROID__
android_async_unlock();
#elif _WIN32
async_unlock();
#endif
release_render_context();
}
void App::async_swap()
{
#if __OSX__
[osx_view async_swap];
#elif __IOS__
[ios_view async_swap];
#elif __ANDROID__
android_async_swap();
#elif _WIN32
win32_async_swap();
#elif __LINUX__ || __WEB__
glfwSwapBuffers(glfw_window);
#endif
present_render_context();
}
bool App::update_ui_observer(Node *n)
@@ -569,7 +585,13 @@ bool App::update_ui_observer(Node *n)
n->m_on_screen = true;
}
glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom;
glScissor(floorf(c.x + off_x), floorf(c.y + off_y), ceilf(c.z), ceilf(c.w));
apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
.enabled = 1U,
.x = static_cast<std::int32_t>(floorf(c.x + off_x)),
.y = static_cast<std::int32_t>(floorf(c.y + off_y)),
.width = static_cast<std::int32_t>(ceilf(c.z)),
.height = static_cast<std::int32_t>(ceilf(c.w)),
});
n->draw();
return true;
}
@@ -588,32 +610,36 @@ void App::draw(float dt)
{
uirtt.bindFramebuffer();
uirtt.clear();
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
glEnable(GL_SCISSOR_TEST);
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
.width = static_cast<std::int32_t>(uirtt.getWidth()),
.height = static_cast<std::int32_t>(uirtt.getHeight()),
});
apply_app_scissor_test(true);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_app_scissor_test(false);
uirtt.unbindFramebuffer();
}
if (!vr_only)
{
#if __IOS__
[ios_view->glview bindDrawable];
#else
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
glEnable(GL_SCISSOR_TEST);
bind_main_render_target();
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
.x = static_cast<std::int32_t>(off_x),
.y = static_cast<std::int32_t>(off_y),
.width = static_cast<std::int32_t>(width),
.height = static_cast<std::int32_t>(height),
});
apply_app_scissor_test(true);
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_app_scissor_test(false);
}
redraw = false;
@@ -636,25 +662,26 @@ void App::update(float dt)
main->update(width, height, zoom);
{
static glm::vec4 color_button_normal{ .1, .1, .1, 1 };
static glm::vec4 color_button_hlight{ 1, .0, .0, 1 };
auto mode = Canvas::I->m_current_mode;
CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock);
const auto toolbar = pp::app::plan_canvas_tool_button_state(
canvas_tool_mode_from_canvas_mode(mode),
pm && pm->m_picking,
canvas->m_canvas->m_touch_lock);
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(mode == kCanvasMode::Draw);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(mode == kCanvasMode::Erase);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(mode == kCanvasMode::FloodFill);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
}
}
@@ -688,9 +715,8 @@ void App::update_memory_usage(size_t bytes)
{
if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
{
static char buffer[128];
sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
txt->set_text(buffer);
const auto label = pp::app::make_history_memory_label(bytes);
txt->set_text(label.c_str());
}
}
@@ -698,91 +724,102 @@ void App::update_rec_frames()
{
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
{
if (rec_running && Canvas::I->m_encoder)
{
static char buffer[128];
sprintf(buffer, "Recorded %d frames", Canvas::I->m_encoder->frames_count());
txt->set_text(buffer);
}
else
{
txt->set_text("");
}
const auto label = pp::app::make_recording_frame_label(
rec_running,
Canvas::I->m_encoder != nullptr,
Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0);
txt->set_text(label.text.c_str());
}
}
int App::res_from_index(int i)
{
return res_map[i];
const auto resolution = pp::app::display_resolution_from_index(i);
return resolution ? resolution.value() : pp::app::document_resolution_values.front();
}
int App::res_to_index(int res)
{
return (int)std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res));
const auto index = pp::app::document_resolution_to_index(res);
return index ? static_cast<int>(index.value()) : static_cast<int>(pp::app::document_resolution_values.size());
}
std::string App::res_to_string(int res)
{
return res_map_str[res_to_index(res)];
const auto label = pp::app::document_resolution_label(res);
return label ? std::string(label.value()) : std::string("unknown");
}
void App::renderdoc_frame_start()
{
#if __WIN__
win32_renderdoc_frame_start();
#endif
begin_render_capture_frame();
}
void App::renderdoc_frame_end()
{
#if __WIN__
win32_renderdoc_frame_end();
#endif
end_render_capture_frame();
}
void App::rec_clear()
{
rec_stop();
#if defined(__IOS__) || defined(__OSX__)
delete_all_files_in_path(rec_path);
#endif
rec_count = 0;
const auto plan = pp::app::plan_recording_clear(
rec_running,
platform_deletes_recorded_files_on_clear()
);
if (plan.stop_running_recording)
rec_stop();
if (plan.delete_recorded_files)
clear_platform_recorded_files(rec_path);
rec_count = plan.frame_count_after_clear;
update_rec_frames();
}
void App::rec_start()
{
if (!rec_running)
const auto plan = pp::app::plan_recording_start(rec_running);
switch (plan)
{
update_rec_frames();
rec_thread = std::thread(&App::rec_loop, this);
case pp::app::RecordingStartAction::start_thread:
break;
case pp::app::RecordingStartAction::no_op_already_running:
return;
}
update_rec_frames();
rec_thread = std::thread(&App::rec_loop, this);
}
void App::rec_stop()
{
if (rec_running)
const auto plan = pp::app::plan_recording_stop(rec_running);
switch (plan)
{
rec_running = false;
rec_cv.notify_all();
if (rec_thread.joinable())
rec_thread.join();
update_rec_frames();
case pp::app::RecordingStopAction::stop_thread:
break;
case pp::app::RecordingStopAction::no_op_not_running:
return;
}
rec_running = false;
rec_cv.notify_all();
if (rec_thread.joinable())
rec_thread.join();
update_rec_frames();
}
void App::rec_export(std::string path)
{
int progress = 0;
int tot = rec_count;
const auto plan = pp::app::plan_recording_export(static_cast<std::size_t>(rec_count));
auto pb = layout[main_id]->add_child<NodeProgressBar>();
pb->m_progress->SetWidthP(0);
pb->m_title->set_text("Exporting MP4 movie");
pb->m_total = plan.progress_total;
pb->m_count = 0;
/*
#if defined(__IOS__) || defined(__OSX__)
export_mp4(rec_path, width, height, rec_count, ^(float) {
pb->m_progress->SetWidthP((float)progress / tot * 100.f);
pb->increment();
});
#endif
*/
@@ -915,7 +952,7 @@ void App::ui_thread_tick()
update(0);
render_task([this]
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
bind_default_render_target();
clear();
draw(0);
async_swap();
@@ -931,9 +968,7 @@ void App::ui_thread_main()
ui_thread_id = std::this_thread::get_id();
ui_running = true;
#if __ANDROID__
android_attach_jni();
#endif
attach_ui_thread();
LOG("ui thread init()");
init();
@@ -971,10 +1006,7 @@ void App::ui_thread_main()
float dt = std::chrono::duration<float>(t_now - t_start).count();
t_start = t_now;
#ifdef _WIN32
extern void win32_update_stylus(float dt);
win32_update_stylus(dt);
#endif
update_platform_frame(dt);
// increment timers
t_frame += dt;
@@ -982,33 +1014,28 @@ void App::ui_thread_main()
if (t_fps_counter > 1.f)
{
#ifdef _WIN32
extern void win32_update_fps(int frames);
win32_update_fps(rendered_frames);
#elif __LINUX__
extern void linux_update_fps(int frames);
linux_update_fps(rendered_frames);
#endif
report_rendered_frames(rendered_frames);
t_fps_counter = 0;
rendered_frames = 0;
}
#if /*_DEBUG &&*/ (_WIN32 || __OSX__)
t_reloader += dt;
if (t_reloader > 1.0)
if (platform_enables_live_asset_reloading())
{
t_reloader = 0;
if (ShaderManager::reload())
t_reloader += dt;
if (t_reloader > 1.0)
{
stroke->update_controls();
redraw = true;
t_reloader = 0;
if (ShaderManager::reload())
{
stroke->update_controls();
redraw = true;
}
if (layout.reload())
redraw = true;
if (layout_designer.reload())
redraw = true;
}
if (layout.reload())
redraw = true;
if (layout_designer.reload())
redraw = true;
}
#endif
tick(dt);
@@ -1017,7 +1044,7 @@ void App::ui_thread_main()
update(t_frame);
render_task([this, t_frame]
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
bind_default_render_target();
clear();
draw(t_frame);
async_swap();
@@ -1026,9 +1053,7 @@ void App::ui_thread_main()
rendered_frames++;
}
}
#if __ANDROID__
android_detach_jni();
#endif
detach_ui_thread();
}
void App::render_thread_start()

View File

@@ -24,6 +24,12 @@
#include "node_input_box.h"
#include "node_panel_animation.h"
#include "layout.h"
#include "app_core/document_session.h"
namespace pp::platform {
class PlatformServices;
struct PlatformStoragePaths;
}
#if defined(__OBJC__) && defined(__IOS__)
#import <Foundation/Foundation.h>
@@ -155,6 +161,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;
@@ -183,6 +190,30 @@ public:
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();
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);
[[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 +279,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();

View File

@@ -1,19 +1,29 @@
#include "pch.h"
#include "app.h"
#include "app_core/document_cloud.h"
#include "util.h"
#include "node_progress_bar.h"
#include "node_dialog_cloud.h"
void App::cloud_upload()
{
if (!canvas)
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);
switch (plan.action)
{
case pp::app::CloudUploadAction::unavailable_no_canvas:
return;
if (Canvas::I->m_newdoc)
{
case pp::app::CloudUploadAction::show_save_required_warning:
message_box("Warning", "This document needs to be saved before upload.");
return;
case pp::app::CloudUploadAction::prompt_publish:
break;
}
else
{
auto upload_thread = [this] {
BT_SetTerminate();
@@ -42,7 +52,6 @@ void App::cloud_upload()
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
m->destroy();
};
}
}
void App::cloud_upload_all()
@@ -51,22 +60,23 @@ 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());
if (plan.show_progress)
pb = show_progress("Export Pano Image", plan.progress_total);
for (const auto& n : names)
{
std::string path = data_path + "/" + n;
upload(path);
if (layout.m_loaded)
if (plan.show_progress)
pb->increment();
}
if (layout.m_loaded)
if (plan.show_progress)
pb->destroy();
}).detach();
@@ -74,8 +84,14 @@ void App::cloud_upload_all()
void App::cloud_browse()
{
if (!canvas)
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
switch (browse_plan)
{
case pp::app::CloudBrowseAction::unavailable_no_canvas:
return;
case pp::app::CloudBrowseAction::show_browser:
break;
}
// load thumbnail test
auto dialog = std::make_shared<NodeDialogCloud>();
@@ -88,7 +104,8 @@ void App::cloud_browse()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
if (dialog->selected_file.empty())
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
if (selection_plan == pp::app::CloudDownloadSelectionAction::wait_for_selection)
return;
dialog->destroy();
std::thread([this, dialog] {

View File

@@ -1,16 +1,46 @@
#include "pch.h"
#include "app.h"
#include "canvas.h"
#include "renderer_gl/opengl_capabilities.h"
namespace {
[[nodiscard]] GLenum depth_test_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}
[[nodiscard]] GLenum program_point_size_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::program_point_size_state());
}
[[nodiscard]] GLenum source_alpha_blend_factor() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::source_alpha_blend_factor());
}
[[nodiscard]] GLenum one_minus_source_alpha_blend_factor() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::one_minus_source_alpha_blend_factor());
}
[[nodiscard]] GLenum add_blend_equation() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::add_blend_equation());
}
}
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);
glDisable(depth_test_state());
glEnable(program_point_size_state());
glBlendFunc(source_alpha_blend_factor(), one_minus_source_alpha_blend_factor());
glBlendEquation(add_blend_equation());
Canvas* canvas = new Canvas;
canvas->create(CANVAS_RES, CANVAS_RES);
canvas->project_open_thread(pano_path);
canvas->export_equirectangular_thread(out_path);
Canvas* command_canvas = new Canvas;
command_canvas->create(CANVAS_RES, CANVAS_RES);
command_canvas->project_open_thread(pano_path);
command_canvas->export_equirectangular_thread(out_path);
}

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");
}
}

View File

@@ -0,0 +1,114 @@
#pragma once
#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;
};
[[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 StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
{
return { mode };
}
}

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

@@ -0,0 +1,119 @@
#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;
};
[[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,
};
}
}

628
src/app_core/brush_ui.h Normal file
View File

@@ -0,0 +1,628 @@
#pragma once
#include "foundation/result.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <string_view>
namespace pp::app {
enum class BrushUiTextureSlot {
tip,
pattern,
dual,
};
enum class BrushUiOperation {
set_tip_color,
set_texture,
replace_brush_from_preset,
stroke_settings_changed,
};
enum class BrushTextureListOperation {
add_texture,
remove_texture,
move_texture,
};
enum class BrushStrokeControlOperation {
set_float,
set_bool,
set_blend_mode,
reset_tip_aspect,
reset_default_brush,
};
enum class BrushStrokeFloatSetting {
tip_size,
tip_spacing,
tip_flow,
tip_opacity,
tip_angle,
tip_angle_smooth,
tip_mix,
tip_wet,
tip_noise,
tip_hue,
tip_saturation,
tip_value,
jitter_scale,
jitter_angle,
jitter_scatter,
jitter_flow,
jitter_opacity,
jitter_hue,
jitter_saturation,
jitter_value,
jitter_aspect,
dual_size,
dual_spacing,
dual_scatter,
tip_aspect,
dual_opacity,
dual_flow,
dual_rotate,
pattern_scale,
pattern_brightness,
pattern_contrast,
pattern_depth,
};
enum class BrushStrokeBoolSetting {
tip_angle_init,
tip_angle_follow,
tip_flow_pressure,
tip_opacity_pressure,
tip_size_pressure,
jitter_scatter_both_axis,
jitter_aspect_both_axis,
jitter_hsv_each_sample,
tip_invert,
tip_flip_x,
tip_flip_y,
pattern_enabled,
dual_enabled,
dual_scatter_both_axis,
dual_invert,
dual_flip_x,
dual_flip_y,
dual_random_flip,
tip_random_flip_x,
tip_random_flip_y,
pattern_each_sample,
pattern_invert,
pattern_flip_x,
pattern_flip_y,
pattern_random_offset,
};
enum class BrushStrokeBlendSetting {
tip,
dual,
pattern,
};
struct BrushUiPlan {
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
std::string path;
std::string thumbnail_path;
float r = 0.0F;
float g = 0.0F;
float b = 0.0F;
float a = 1.0F;
bool mutates_brush = false;
bool preserves_existing_color = false;
bool loads_brush_resources = false;
bool update_color_ui = false;
bool update_brush_ui = false;
};
struct BrushTextureListPlan {
BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
int item_count = 0;
int current_index = -1;
int target_index = -1;
int move_offset = 0;
std::string source_path;
std::string high_path;
std::string thumbnail_path;
std::string brush_name;
bool user_texture = false;
bool deletes_texture_files = false;
bool saves_list = false;
bool notifies_selection = false;
bool converts_brush_alpha = false;
bool no_op = false;
};
struct BrushStrokeControlPlan {
BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init;
BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip;
float float_value = 0.0F;
bool bool_value = false;
int blend_mode = 0;
bool mutates_brush = false;
bool updates_controls = false;
bool refreshes_preview = false;
bool notifies_stroke_change = false;
};
class BrushUiServices {
public:
virtual ~BrushUiServices() = default;
virtual void set_tip_color(float r, float g, float b, float a) = 0;
virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};
class BrushTextureListServices {
public:
virtual ~BrushTextureListServices() = default;
virtual pp::foundation::Status add_texture_from_source(
std::string_view source_path,
std::string_view high_path,
std::string_view thumbnail_path,
std::string_view brush_name,
bool converts_brush_alpha) = 0;
virtual void remove_texture(int index, bool delete_texture_files) = 0;
virtual void move_texture(int from_index, int to_index) = 0;
virtual void select_texture(int index) = 0;
virtual void save_texture_list() = 0;
};
class BrushStrokeControlServices {
public:
virtual ~BrushStrokeControlServices() = default;
virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0;
virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0;
virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0;
virtual void reset_tip_aspect(float value) = 0;
virtual void reset_default_brush() = 0;
virtual void update_stroke_controls() = 0;
virtual void refresh_stroke_preview() = 0;
virtual void notify_stroke_changed() = 0;
};
[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
std::string_view source_path) noexcept
{
const auto slash = source_path.find_last_of("/\\");
const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
if (name_begin >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
}
const auto dot = source_path.find_last_of('.');
if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
return pp::foundation::Result<std::string_view>::failure(
pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
}
return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
}
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
{
if (!std::isfinite(value)) {
return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
{
if (blend_mode < 0 || blend_mode > 63) {
return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
float r,
float g,
float b,
float a)
{
for (const auto value : { r, g, b, a }) {
const auto channel_status = validate_brush_ui_color_channel(value);
if (!channel_status.ok()) {
return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
}
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::set_tip_color;
plan.r = r;
plan.g = g;
plan.b = b;
plan.a = a;
plan.mutates_brush = true;
plan.update_color_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
BrushUiTextureSlot slot,
std::string_view path,
std::string_view thumbnail_path)
{
if (path.empty()) {
return pp::foundation::Result<BrushUiPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::set_texture;
plan.texture_slot = slot;
plan.path = std::string(path);
plan.thumbnail_path = std::string(thumbnail_path);
plan.mutates_brush = true;
plan.loads_brush_resources = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
{
if (!has_preset_brush) {
return pp::foundation::Result<BrushUiPlan>::failure(
pp::foundation::Status::invalid_argument("preset brush must be available"));
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::replace_brush_from_preset;
plan.mutates_brush = true;
plan.preserves_existing_color = true;
plan.loads_brush_resources = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
{
BrushUiPlan plan;
plan.operation = BrushUiOperation::stroke_settings_changed;
plan.mutates_brush = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
BrushStrokeFloatSetting setting,
float value)
{
const auto status = validate_brush_stroke_float(value);
if (!status.ok()) {
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
}
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_float;
plan.float_setting = setting;
plan.float_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
BrushStrokeBoolSetting setting,
bool value) noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_bool;
plan.bool_setting = setting;
plan.bool_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
BrushStrokeBlendSetting setting,
int blend_mode)
{
const auto status = validate_brush_stroke_blend_mode(blend_mode);
if (!status.ok()) {
return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
}
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::set_blend_mode;
plan.blend_setting = setting;
plan.blend_mode = blend_mode;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
plan.float_value = value;
plan.mutates_brush = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
{
BrushStrokeControlPlan plan;
plan.operation = BrushStrokeControlOperation::reset_default_brush;
plan.mutates_brush = true;
plan.updates_controls = true;
plan.refreshes_preview = true;
plan.notifies_stroke_change = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
std::string_view directory_name,
std::string_view data_path,
std::string_view source_path)
{
if (directory_name.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
}
if (data_path.empty()) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
}
const auto stem = brush_texture_source_stem(source_path);
if (!stem) {
return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::add_texture;
plan.source_path = std::string(source_path);
plan.brush_name = std::string(stem.value());
plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
+ plan.brush_name + ".png";
plan.user_texture = true;
plan.saves_list = true;
plan.converts_brush_alpha = directory_name == "brushes";
return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
int item_count,
int current_index,
bool current_is_user_texture)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::remove_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
plan.user_texture = current_is_user_texture;
plan.deletes_texture_files = current_is_user_texture;
plan.saves_list = true;
plan.notifies_selection = plan.target_index >= 0;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
int item_count,
int current_index,
int offset)
{
if (item_count <= 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
}
if (current_index < 0 || current_index >= item_count) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
}
if (offset == 0) {
return pp::foundation::Result<BrushTextureListPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
}
BrushTextureListPlan plan;
plan.operation = BrushTextureListOperation::move_texture;
plan.item_count = item_count;
plan.current_index = current_index;
plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
plan.move_offset = offset;
plan.saves_list = true;
plan.no_op = plan.target_index == current_index;
return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan,
BrushUiServices& services)
{
switch (plan.operation) {
case BrushUiOperation::set_tip_color:
{
for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
const auto channel_status = validate_brush_ui_color_channel(value);
if (!channel_status.ok()) {
return channel_status;
}
}
services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
case BrushUiOperation::set_texture:
if (plan.path.empty()) {
return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
}
services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::replace_brush_from_preset:
services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::stroke_settings_changed:
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
}
[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
const BrushStrokeControlPlan& plan,
BrushStrokeControlServices& services)
{
switch (plan.operation) {
case BrushStrokeControlOperation::set_float:
{
const auto status = validate_brush_stroke_float(plan.float_value);
if (!status.ok()) {
return status;
}
services.set_float_setting(plan.float_setting, plan.float_value);
break;
}
case BrushStrokeControlOperation::set_bool:
services.set_bool_setting(plan.bool_setting, plan.bool_value);
break;
case BrushStrokeControlOperation::set_blend_mode:
{
const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
if (!status.ok()) {
return status;
}
services.set_blend_mode(plan.blend_setting, plan.blend_mode);
break;
}
case BrushStrokeControlOperation::reset_tip_aspect:
{
const auto status = validate_brush_stroke_float(plan.float_value);
if (!status.ok()) {
return status;
}
services.reset_tip_aspect(plan.float_value);
break;
}
case BrushStrokeControlOperation::reset_default_brush:
services.reset_default_brush();
break;
}
if (plan.updates_controls) {
services.update_stroke_controls();
}
if (plan.refreshes_preview) {
services.refresh_stroke_preview();
}
if (plan.notifies_stroke_change) {
services.notify_stroke_changed();
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
const BrushTextureListPlan& plan,
BrushTextureListServices& services)
{
switch (plan.operation) {
case BrushTextureListOperation::add_texture:
{
if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
|| plan.brush_name.empty()) {
return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
}
const auto add_status = services.add_texture_from_source(
plan.source_path,
plan.high_path,
plan.thumbnail_path,
plan.brush_name,
plan.converts_brush_alpha);
if (!add_status.ok()) {
return add_status;
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
case BrushTextureListOperation::remove_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
}
services.remove_texture(plan.current_index, plan.deletes_texture_files);
if (plan.notifies_selection && plan.target_index >= 0) {
services.select_texture(plan.target_index);
}
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
case BrushTextureListOperation::move_texture:
if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
|| plan.target_index < 0 || plan.target_index >= plan.item_count) {
return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
}
services.move_texture(plan.current_index, plan.target_index);
if (plan.saves_list) {
services.save_texture_list();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
}
} // namespace pp::app

View File

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

View File

@@ -0,0 +1,620 @@
#pragma once
#include "foundation/result.h"
#include <algorithm>
#include <cstdint>
#include <limits>
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;
};
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<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,85 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
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;
};
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<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,79 @@
#pragma once
#include <cstddef>
#include <limits>
#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,
};
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;
};
[[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,
};
}
}

View File

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

View File

@@ -0,0 +1,283 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
struct DocumentExportFileTarget {
std::string path;
std::string suggested_name;
};
struct DocumentExportCollectionTarget {
std::string directory;
std::string stem_path;
};
struct DocumentExportStemTarget {
std::string stem_path;
};
struct DocumentExportSuggestedName {
std::string name;
};
enum class DocumentExportStartDecision {
start_now,
show_license_disabled,
unavailable_no_canvas,
};
enum class DocumentExportMenuKind {
jpeg,
png,
layers,
cube_faces,
depth,
animation_frames,
animation_mp4,
timelapse,
};
enum class DocumentExportMenuAction {
show_jpeg_dialog,
show_png_dialog,
show_layers_dialog,
show_cube_faces_dialog,
show_depth_dialog,
show_animation_frames_dialog,
show_animation_mp4_dialog,
show_timelapse_dialog,
show_license_disabled,
unavailable_no_canvas,
};
struct DocumentExportMenuPlan {
DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg;
DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog;
bool opens_dialog = true;
};
class DocumentExportMenuServices {
public:
virtual ~DocumentExportMenuServices() = default;
virtual void show_jpeg_dialog() = 0;
virtual void show_png_dialog() = 0;
virtual void show_layers_dialog() = 0;
virtual void show_cube_faces_dialog() = 0;
virtual void show_depth_dialog() = 0;
virtual void show_animation_frames_dialog() = 0;
virtual void show_animation_mp4_dialog() = 0;
virtual void show_timelapse_dialog() = 0;
virtual void show_license_disabled() = 0;
};
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
bool requires_license,
bool license_valid,
bool has_canvas) noexcept
{
if (requires_license && !license_valid) {
return DocumentExportStartDecision::show_license_disabled;
}
return has_canvas
? DocumentExportStartDecision::start_now
: DocumentExportStartDecision::unavailable_no_canvas;
}
[[nodiscard]] constexpr bool document_export_menu_requires_license(
DocumentExportMenuKind kind) noexcept
{
switch (kind) {
case DocumentExportMenuKind::animation_mp4:
case DocumentExportMenuKind::timelapse:
return true;
case DocumentExportMenuKind::jpeg:
case DocumentExportMenuKind::png:
case DocumentExportMenuKind::layers:
case DocumentExportMenuKind::cube_faces:
case DocumentExportMenuKind::depth:
case DocumentExportMenuKind::animation_frames:
return false;
}
return false;
}
[[nodiscard]] constexpr DocumentExportMenuAction document_export_menu_dialog_action(
DocumentExportMenuKind kind) noexcept
{
switch (kind) {
case DocumentExportMenuKind::jpeg:
return DocumentExportMenuAction::show_jpeg_dialog;
case DocumentExportMenuKind::png:
return DocumentExportMenuAction::show_png_dialog;
case DocumentExportMenuKind::layers:
return DocumentExportMenuAction::show_layers_dialog;
case DocumentExportMenuKind::cube_faces:
return DocumentExportMenuAction::show_cube_faces_dialog;
case DocumentExportMenuKind::depth:
return DocumentExportMenuAction::show_depth_dialog;
case DocumentExportMenuKind::animation_frames:
return DocumentExportMenuAction::show_animation_frames_dialog;
case DocumentExportMenuKind::animation_mp4:
return DocumentExportMenuAction::show_animation_mp4_dialog;
case DocumentExportMenuKind::timelapse:
return DocumentExportMenuAction::show_timelapse_dialog;
}
return DocumentExportMenuAction::show_jpeg_dialog;
}
[[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action(
DocumentExportMenuKind kind,
bool has_canvas,
bool license_valid) noexcept
{
DocumentExportMenuPlan plan;
plan.kind = kind;
plan.action = document_export_menu_dialog_action(kind);
const auto start = plan_document_export_start(
document_export_menu_requires_license(kind),
license_valid,
has_canvas);
if (start == DocumentExportStartDecision::show_license_disabled) {
plan.action = DocumentExportMenuAction::show_license_disabled;
plan.opens_dialog = false;
} else if (start == DocumentExportStartDecision::unavailable_no_canvas) {
plan.action = DocumentExportMenuAction::unavailable_no_canvas;
plan.opens_dialog = false;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view extension)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
if (extension.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("extension must not be empty"));
}
DocumentExportFileTarget target;
target.suggested_name.reserve(document_name.size() + extension.size());
target.suggested_name += document_name;
target.suggested_name += extension;
target.path.reserve(work_directory.size() + target.suggested_name.size() + 1);
target.path += work_directory;
target.path += "/";
target.path += target.suggested_name;
return pp::foundation::Result<DocumentExportFileTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportCollectionTarget> make_document_export_collection_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportCollectionTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportCollectionTarget target;
target.directory.reserve(work_directory.size() + document_name.size() + suffix.size() + 1);
target.directory += work_directory;
target.directory += "/";
target.directory += document_name;
target.directory += suffix;
target.stem_path.reserve(target.directory.size() + document_name.size() + 1);
target.stem_path += target.directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportCollectionTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportStemTarget> make_document_export_stem_target(
std::string_view directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportStemTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportStemTarget target;
target.stem_path.reserve(directory.size() + document_name.size() + 1);
target.stem_path += directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportStemTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportSuggestedName>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportSuggestedName target;
target.name.reserve(document_name.size() + suffix.size());
target.name += document_name;
target.name += suffix;
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan(
const DocumentExportMenuPlan& plan,
DocumentExportMenuServices& services)
{
switch (plan.action) {
case DocumentExportMenuAction::show_jpeg_dialog:
services.show_jpeg_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_png_dialog:
services.show_png_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_layers_dialog:
services.show_layers_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_cube_faces_dialog:
services.show_cube_faces_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_depth_dialog:
services.show_depth_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_animation_frames_dialog:
services.show_animation_frames_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_animation_mp4_dialog:
services.show_animation_mp4_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_timelapse_dialog:
services.show_timelapse_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_license_disabled:
services.show_license_disabled();
return pp::foundation::Status::success();
case DocumentExportMenuAction::unavailable_no_canvas:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document export menu action");
}
}

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,448 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
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;
};
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;
};
[[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<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::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,63 @@
#pragma once
#include <cstddef>
#include <limits>
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;
};
[[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),
};
}
}

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,323 @@
#pragma once
#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,
};
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;
};
struct NewDocumentPlan {
DocumentFileTarget target;
int resolution = 0;
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};
[[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]] 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]] 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]] 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::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]] 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 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"));
}
}

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

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

@@ -0,0 +1,145 @@
#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;
};
[[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;
}
} // 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

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

@@ -0,0 +1,202 @@
#pragma once
#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 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

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

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

View File

@@ -1,6 +1,10 @@
#include "pch.h"
#include "app.h"
#include "action.h"
#include "app_core/document_layer.h"
#include "app_core/document_resize.h"
#include "app_core/document_export.h"
#include "app_core/document_session.h"
#include "settings.h"
#include "node_dialog_open.h"
#include "node_dialog_browse.h"
@@ -21,11 +25,33 @@
#include "oculus_vr.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]] bool can_start_document_export(App& app, bool requires_license)
{
const auto decision = pp::app::plan_document_export_start(
requires_license,
!requires_license || app.check_license(),
app.canvas != nullptr);
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return true;
case pp::app::DocumentExportStartDecision::show_license_disabled:
app.message_box("License", "This function is disabled in demo mode.");
return false;
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return false;
}
return false;
}
}
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
{
auto pb = std::make_shared<NodeProgressBar>();
@@ -105,6 +131,41 @@ void App::dialog_about()
layout[main_id]->add_child(dialog);
}
void App::continue_document_workflow_after_optional_save(std::function<void()> action)
{
const bool has_canvas = canvas != nullptr;
const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved;
const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes);
switch (decision) {
case pp::app::DocumentWorkflowDecision::unavailable:
return;
case pp::app::DocumentWorkflowDecision::continue_now:
action();
return;
case pp::app::DocumentWorkflowDecision::prompt_save_before_continue:
break;
}
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, action](Node*) {
Canvas::I->project_save([this, m, action](bool success) {
if (success)
action();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [m, action](Node*) {
action();
m->destroy();
};
}
void App::dialog_newdoc()
{
auto show_dialog = [this] {
@@ -122,25 +183,32 @@ void App::dialog_newdoc()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi";
if (name.empty())
const auto plan = pp::app::plan_new_document(
work_path,
name,
dialog->m_resolution->m_current_index,
[](const std::string& path) {
return Asset::exist(path);
});
if (!plan)
{
message_box("Warning", "You need to specify a name to file.");
const bool missing_name =
plan.status().code == pp::foundation::StatusCode::invalid_argument;
message_box(
"Warning",
missing_name ? "You need to specify a name to file." : plan.status().message);
return;
}
auto action = [this, dialog, name, path] {
std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
int res = resolutions[dialog->m_resolution->m_current_index];
doc_name = name;
doc_path = path;
doc_filename = name + ".ppi";
doc_dir = work_path;
auto action = [this, dialog, plan = plan.value()] {
doc_name = plan.target.name;
doc_path = plan.target.path;
doc_filename = plan.target.name + ".ppi";
doc_dir = plan.target.directory;
layers->clear();
canvas->m_canvas->m_layers.clear();
canvas->m_canvas->resize(res, res);
canvas->m_canvas->resize(plan.resolution, plan.resolution);
canvas->reset_camera();
ActionManager::clear();
@@ -154,7 +222,7 @@ void App::dialog_newdoc()
App::I->hideKeyboard();
};
if (Asset::exist(path))
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{
// ask confirm is file already exist
auto msgbox = new NodeMessageBox();
@@ -181,34 +249,7 @@ void App::dialog_newdoc()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
Canvas::I->project_save([this, m, show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
// DEPRECATED
@@ -242,34 +283,7 @@ void App::dialog_open()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this,m,show_dialog](Node*){
Canvas::I->project_save([this,m,show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this,m,show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
void App::dialog_browse()
@@ -299,34 +313,7 @@ void App::dialog_browse()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
Canvas::I->project_save([this, m, show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
void App::dialog_save_ver()
@@ -337,35 +324,45 @@ void App::dialog_save_ver()
return;
}
int current = 0;
std::string next = doc_name + ".01";
std::string base = doc_name;
std::regex r(R"((.*)\.(\w{2})$)");
std::smatch m;
if (std::regex_search(doc_name, m, r))
{
base = m[1].str();
current = atoi(m[2].str().c_str());
const auto target = pp::app::find_next_document_version_target(
doc_dir,
doc_name,
[](const std::string& path) {
return Asset::exist(path);
});
if (!target) {
message_box("Saving Error", target.status().message);
return;
}
for (int i = current + 1; i < 99; i++)
{
static char tmp_name[256];
sprintf(tmp_name, "%s.%02d", base.c_str(), i);
next = tmp_name;
if (Asset::exist(doc_dir + "/" + next + ".ppi"))
continue;
break;
}
doc_name = next;
doc_path = doc_dir + "/" + next + ".ppi";
doc_name = target.value().name;
doc_path = target.value().path;
canvas->m_canvas->m_unsaved = true;
title_update();
canvas->m_canvas->project_save(doc_path);
}
void App::save_document(pp::app::DocumentSaveIntent intent)
{
const auto decision = pp::app::plan_document_save(
Canvas::I->m_newdoc,
Canvas::I->m_unsaved,
intent);
switch (decision) {
case pp::app::DocumentSaveDecision::show_save_dialog:
dialog_save();
break;
case pp::app::DocumentSaveDecision::save_existing:
Canvas::I->project_save();
break;
case pp::app::DocumentSaveDecision::save_version:
dialog_save_ver();
break;
case pp::app::DocumentSaveDecision::no_op:
break;
}
}
void App::dialog_save()
{
if (!check_license())
@@ -388,32 +385,36 @@ void App::dialog_save()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi";
if (name.empty())
const auto plan = pp::app::plan_document_file_save(
work_path,
name,
[](const std::string& path) {
return Asset::exist(path);
});
if (!plan)
{
message_box("Warning", "You need to specify a name to file.");
return;
}
auto action = [this, dialog, name, path] {
canvas->m_canvas->project_save(path);
doc_name = name;
doc_path = path;
doc_dir = work_path;
auto action = [this, dialog, plan = plan.value()] {
canvas->m_canvas->project_save(plan.target.path);
doc_name = plan.target.name;
doc_path = plan.target.path;
doc_dir = plan.target.directory;
title_update();
dialog->destroy();
App::I->hideKeyboard();
};
if (Asset::exist(path))
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{
// ask confirm is file already exist
auto msgbox = new NodeMessageBox();
msgbox->set_manager(&layout);
msgbox->init();
msgbox->m_title->set_text("Warning");
msgbox->m_message->set_text(("Are you sure you want to overwrite " + name + "?").c_str());
msgbox->m_message->set_text(("Are you sure you want to overwrite " + plan.value().target.name + "?").c_str());
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
action();
msgbox->destroy();
@@ -437,120 +438,145 @@ void App::dialog_save()
void App::dialog_export(std::string ext)
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
if (!target) {
message_box("Export Equirectangular", target.status().message);
return;
}
if (canvas)
{
// TODO: use picker
auto path = work_path + "/" + doc_name + ext;
auto name = doc_name + ext;
canvas->m_canvas->export_equirectangular(path, [this, path, name]{
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
#if defined(__IOS__)
message_box("Export Equirectangular", "Image exported to Photos");
message_box("Export Equirectangular", "Image exported to Photos");
#elif defined(__OSX__)
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
message_box("Export Equirectangular", "Image exported to Pictures/PanoPainter folder");
#elif defined(_WIN32)
message_box("Export Equirectangular", "Image exported to " + work_path);
message_box("Export Equirectangular", "Image exported to " + work_path);
#elif defined(__QUEST__)
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
#elif __WEB__
ui_task([=]{
webgl_pick_file_save(path, name, [](bool success){ });
});
#endif
ui_task([=]{
save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
});
}
#endif
});
}
void App::dialog_export_layers()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (canvas)
if (Asset::create_dir(target.value().directory))
{
#if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_layers";
if (Asset::create_dir(dir))
{
auto p = dir + "/" + doc_name;
canvas->m_canvas->export_layers(p, [this, p] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
auto p = path + "/" + doc_name;
canvas->m_canvas->export_layers(p, [this, p] {
message_box("Export Layers", "Layers exported to: " + p);
});
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
#endif
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
message_box("Export Layers", "Layers exported to: " + target.stem_path);
});
});
#endif
}
void App::dialog_export_anim_frames()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (canvas)
if (Asset::create_dir(target.value().directory))
{
#if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_frames";
if (Asset::create_dir(dir))
{
auto p = dir + "/" + doc_name;
canvas->m_canvas->export_anim_frames(p, [this, p] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
auto p = path + "/" + doc_name;
canvas->m_canvas->export_anim_frames(p, [this, p] {
message_box("Export Layers", "Layers exported to: " + p);
});
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
#endif
}
#else
pick_dir([this](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] {
message_box("Export Layers", "Layers exported to: " + target.stem_path);
});
});
#endif
}
void App::dialog_export_depth()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
}
if (canvas)
{
// TODO: use picker
canvas->m_canvas->export_depth(doc_name, [this] {
// TODO: use picker
canvas->m_canvas->export_depth(doc_name, [this] {
#if defined(__IOS__)
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
message_box("Export 3D View + Depth", "Image and depth exported to Files/PanoPainter");
#elif defined(__OSX__)
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
message_box("Export 3D View + Depth", "Image and depth exported to Pictures/PanoPainter folder");
#elif defined(_WIN32)
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
#endif
});
}
});
}
void App::dialog_resize()
{
class LegacyDocumentResizeServices final : public pp::app::DocumentResizeServices {
public:
explicit LegacyDocumentResizeServices(App& app) noexcept
: app_(app)
{
}
void resize_document(int width, int height) override
{
if (app_.canvas)
app_.canvas->m_canvas->resize(width, height);
}
void update_title() override
{
app_.title_update();
}
void clear_history() override
{
ActionManager::clear();
}
private:
App& app_;
};
auto dialog = std::make_shared<NodeDialogResize>();
dialog->set_manager(&layout);
dialog->init();
@@ -561,29 +587,35 @@ void App::dialog_resize()
dialog->btn_ok->on_click = [this,dialog](Node*)
{
int res = dialog->get_resolution();
if (canvas)
canvas->m_canvas->resize(res, res);
App::I->title_update();
ActionManager::clear();
const auto plan = pp::app::plan_document_resize(
dialog->combo ? dialog->combo->m_current_index : 0);
if (!plan)
{
dialog->destroy();
return;
}
LegacyDocumentResizeServices services(*this);
const auto status = pp::app::execute_document_resize_plan(plan.value(), services);
if (!status.ok())
LOG("Document resize failed: %s", status.message);
dialog->destroy();
};
}
void App::dialog_export_cube_faces()
{
if (canvas)
{
canvas->m_canvas->export_cube_faces(doc_name, [this] {
if (!can_start_document_export(*this, false))
return;
canvas->m_canvas->export_cube_faces(doc_name, [this] {
#if defined(__IOS__)
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
#elif defined(__OSX__)
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
message_box("Export Cube Faces", "Image and depth exported to Pictures/PanoPainter folder");
#elif defined(_WIN32)
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
#endif
});
}
});
}
void App::dialog_layer_rename()
@@ -601,6 +633,17 @@ void App::dialog_layer_rename()
dialog->btn_ok->on_click = [this,dialog](Node*)
{
const auto old_name = layers->m_current_layer->m_label_text;
const auto plan = pp::app::plan_document_layer_rename(old_name, dialog->get_name());
if (!plan)
return;
if (plan.value().action == pp::app::DocumentLayerRenameAction::no_op_same_name)
{
dialog->destroy();
App::I->hideKeyboard();
return;
}
struct ActionLayerRename : public Action
{
std::string m_old_name;
@@ -624,9 +667,13 @@ void App::dialog_layer_rename()
};
auto layer_node = std::static_pointer_cast<NodeLayer>(layers->m_current_layer->shared_from_this());
auto* layer = canvas->m_canvas->m_layers[canvas->m_canvas->m_current_layer_idx].get();
ActionManager::add(new ActionLayerRename(layers->m_current_layer->m_label_text, dialog->get_name(), layer_node, layer));
layer_node->set_name(dialog->get_name().c_str());
layer->m_name = dialog->get_name();
ActionManager::add(new ActionLayerRename(
plan.value().old_name,
plan.value().new_name,
layer_node,
layer));
layer_node->set_name(plan.value().new_name.c_str());
layer->m_name = plan.value().new_name;
dialog->destroy();
App::I->hideKeyboard();
};
@@ -681,8 +728,17 @@ void App::dialog_ppbr_export()
void App::dialog_timelapse_export()
{
if (!can_start_document_export(*this, false))
return;
#if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-timelapse",
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
if (!target) {
message_box("Export Timelapse", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
rec_export(path);
},
@@ -703,8 +759,17 @@ void App::dialog_timelapse_export()
void App::dialog_export_mp4()
{
if (!can_start_document_export(*this, false))
return;
#if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-animation",
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
if (!target) {
message_box("Export Animation", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
export_anim_mp4(path);
},

View File

@@ -1,76 +1,90 @@
#include "pch.h"
#include "app.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)
@@ -84,7 +98,7 @@ void App::tick(float 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);
uirtt.create(static_cast<int>(w), static_cast<int>(h), -1, rgba8_internal_format(), true);
redraw = true;
width = w;
height = h;
@@ -92,123 +106,50 @@ void App::resize(float w, float h)
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__
@@ -220,10 +161,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
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);
save_prepared_file(path, default_name + ext, callback);
}).detach();
}
#elif __WEB__
@@ -234,110 +172,137 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
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);
save_prepared_file(path, default_name + "." + type, std::move(callback));
}
#else
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
{
redraw = true;
#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));
}
#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));
}
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();
}
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);
}
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)

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,72 @@
#include "pch.h"
#include "app.h"
#include "renderer_api/shader_catalog.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
namespace {
[[nodiscard]] GLenum extension_count_query() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::extension_count_query());
}
[[nodiscard]] GLenum extension_string_name() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
}
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
{
pp::renderer::gl::OpenGlCapabilities capabilities;
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
capabilities.float32_textures = ShaderManager::ext_float32;
capabilities.float32_linear = ShaderManager::ext_float32_linear;
capabilities.float16_textures = ShaderManager::ext_float16;
return capabilities;
}
}
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);
glGetIntegerv(extension_count_query(), &n_exts);
std::vector<std::string> extension_storage;
std::vector<std::string_view> extension_views;
extension_storage.reserve(n_exts);
extension_views.reserve(n_exts);
for (int i = 0; i < n_exts; i++)
{
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());
extension_storage.emplace_back((const char*)glGetStringi(extension_string_name(), i));
extension_views.push_back(extension_storage.back());
LOG("EXT: %s", extension_storage.back().c_str());
}
pp::renderer::gl::OpenGlRuntime runtime;
#if __GL__
runtime.desktop_gl = true;
#endif
#if __GLES__
runtime.gles = true;
#endif
#if __WEB__
runtime.web = true;
#endif
const auto capabilities = pp::renderer::gl::detect_opengl_capabilities(extension_views, runtime);
ShaderManager::ext_framebuffer_fetch = capabilities.framebuffer_fetch;
ShaderManager::ext_map_aligned = capabilities.map_buffer_alignment;
ShaderManager::ext_float32 = capabilities.float32_textures;
ShaderManager::ext_float32_linear = capabilities.float32_linear;
ShaderManager::ext_float16 = capabilities.float16_textures;
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
});
#if __GL__
@@ -41,60 +75,25 @@ void App::initShaders()
ShaderManager::ext_float32 = true;
ShaderManager::ext_float16 = true;
#endif
ShaderManager::set_render_device_features(
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
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,13 +1,98 @@
#include "pch.h"
#include <cstdint>
#include "app.h"
#include "util.h"
#include "shape.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef _WIN32
bool win32_vr_start();
void win32_vr_stop();
#endif
namespace {
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
void unbind_texture_2d()
{
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
}
void enable_opengl_state(std::uint32_t state) noexcept
{
glEnable(static_cast<GLenum>(state));
}
void disable_opengl_state(std::uint32_t state) noexcept
{
glDisable(static_cast<GLenum>(state));
}
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void clear_opengl_mask(std::uint32_t mask) noexcept
{
glClear(static_cast<GLbitfield>(mask));
}
void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
const auto status = pp::renderer::gl::apply_opengl_viewport(
viewport,
pp::renderer::gl::OpenGlViewportDispatch {
.viewport = set_opengl_viewport,
});
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 = enable_opengl_state,
.disable = 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 = enable_opengl_state,
.disable = disable_opengl_state,
});
if (!status.ok())
LOG("OpenGL VR render state failed: %s", status.message);
}
void clear_vr_depth_buffer()
{
const auto status = pp::renderer::gl::clear_opengl_buffers(
pp::renderer::gl::framebuffer_depth_buffer_mask(),
pp::renderer::gl::OpenGlBufferClearDispatch {
.clear = clear_opengl_mask,
});
if (!status.ok())
LOG("OpenGL VR depth clear failed: %s", status.message);
}
}
bool trigger_down = false;
cbuffer<glm::vec3> controller_points(10);
glm::vec3 controller_last_point;
@@ -37,13 +122,16 @@ 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 +273,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);
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
auto depth = glIsEnabled(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 +293,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 +329,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 +380,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 +413,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 +440,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 +465,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 +487,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 +539,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 +554,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 +567,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 != 0U);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth != 0U);
sampler.unbind();
}

View File

@@ -0,0 +1,94 @@
#include "assets/image_format.h"
#include <array>
#include <cstdint>
namespace pp::assets {
namespace {
constexpr std::array png_signature {
std::byte { 0x89 },
std::byte { 0x50 },
std::byte { 0x4e },
std::byte { 0x47 },
std::byte { 0x0d },
std::byte { 0x0a },
std::byte { 0x1a },
std::byte { 0x0a },
};
[[nodiscard]] bool starts_with(std::span<const std::byte> bytes, std::span<const std::byte> prefix) noexcept
{
if (bytes.size() < prefix.size()) {
return false;
}
for (std::size_t i = 0; i < prefix.size(); ++i) {
if (bytes[i] != prefix[i]) {
return false;
}
}
return true;
}
[[nodiscard]] bool is_prefix_of(std::span<const std::byte> bytes, std::span<const std::byte> signature) noexcept
{
if (bytes.size() >= signature.size()) {
return false;
}
for (std::size_t i = 0; i < bytes.size(); ++i) {
if (bytes[i] != signature[i]) {
return false;
}
}
return true;
}
}
pp::foundation::Result<ImageFormat> detect_image_format(std::span<const std::byte> bytes) noexcept
{
if (bytes.empty()) {
return pp::foundation::Result<ImageFormat>::failure(
pp::foundation::Status::invalid_argument("image data must not be empty"));
}
if (starts_with(bytes, png_signature)) {
return pp::foundation::Result<ImageFormat>::success(ImageFormat::png);
}
if (is_prefix_of(bytes, png_signature)) {
return pp::foundation::Result<ImageFormat>::failure(
pp::foundation::Status::out_of_range("image data is a truncated PNG signature"));
}
if (bytes.size() < 3U) {
return pp::foundation::Result<ImageFormat>::failure(
pp::foundation::Status::out_of_range("image data is too short to identify"));
}
if (bytes[0] == std::byte { 0xff } && bytes[1] == std::byte { 0xd8 } && bytes[2] == std::byte { 0xff }) {
return pp::foundation::Result<ImageFormat>::success(ImageFormat::jpeg);
}
return pp::foundation::Result<ImageFormat>::failure(
pp::foundation::Status::invalid_argument("unsupported image signature"));
}
const char* image_format_name(ImageFormat format) noexcept
{
switch (format) {
case ImageFormat::png:
return "png";
case ImageFormat::jpeg:
return "jpeg";
}
return "unknown";
}
}

20
src/assets/image_format.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <span>
namespace pp::assets {
enum class ImageFormat {
png,
jpeg,
};
[[nodiscard]] pp::foundation::Result<ImageFormat> detect_image_format(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] const char* image_format_name(ImageFormat format) noexcept;
}

View File

@@ -0,0 +1,146 @@
#include "assets/image_metadata.h"
#include <cstddef>
namespace pp::assets {
namespace {
constexpr std::byte png_signature[] {
std::byte { 0x89 },
std::byte { 0x50 },
std::byte { 0x4e },
std::byte { 0x47 },
std::byte { 0x0d },
std::byte { 0x0a },
std::byte { 0x1a },
std::byte { 0x0a },
};
[[nodiscard]] bool has_png_signature(std::span<const std::byte> bytes) noexcept
{
if (bytes.size() < 8U) {
return false;
}
for (std::size_t i = 0; i < 8U; ++i) {
if (bytes[i] != png_signature[i]) {
return false;
}
}
return true;
}
[[nodiscard]] std::uint32_t read_u32_be(std::span<const std::byte> bytes, std::size_t offset) noexcept
{
return (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset])) << 24U)
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 1U])) << 16U)
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 2U])) << 8U)
| static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 3U]));
}
[[nodiscard]] pp::foundation::Result<ImageColorType> parse_png_color_type(std::byte value) noexcept
{
switch (std::to_integer<std::uint8_t>(value)) {
case 0:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale);
case 2:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgb);
case 3:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::indexed);
case 4:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale_alpha);
case 6:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgba);
default:
return pp::foundation::Result<ImageColorType>::failure(
pp::foundation::Status::invalid_argument("PNG color type is unsupported"));
}
}
[[nodiscard]] std::uint8_t component_count(ImageColorType color_type) noexcept
{
switch (color_type) {
case ImageColorType::grayscale:
case ImageColorType::indexed:
return 1;
case ImageColorType::grayscale_alpha:
return 2;
case ImageColorType::rgb:
return 3;
case ImageColorType::rgba:
return 4;
}
return 0;
}
}
pp::foundation::Result<ImageMetadata> parse_png_metadata(std::span<const std::byte> bytes) noexcept
{
constexpr std::size_t png_ihdr_end = 33;
if (bytes.size() < png_ihdr_end) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::out_of_range("PNG metadata is truncated"));
}
if (!has_png_signature(bytes)) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG signature is invalid"));
}
const auto ihdr_length = read_u32_be(bytes, 8);
if (ihdr_length != 13U || bytes[12] != std::byte { 'I' } || bytes[13] != std::byte { 'H' }
|| bytes[14] != std::byte { 'D' } || bytes[15] != std::byte { 'R' }) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG IHDR chunk is invalid"));
}
const auto width = read_u32_be(bytes, 16);
const auto height = read_u32_be(bytes, 20);
if (width == 0 || height == 0 || width > max_image_dimension || height > max_image_dimension) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::out_of_range("PNG dimensions are outside the configured range"));
}
const auto bit_depth = std::to_integer<std::uint8_t>(bytes[24]);
if (bit_depth == 0U) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG bit depth is invalid"));
}
const auto color_type = parse_png_color_type(bytes[25]);
if (!color_type) {
return pp::foundation::Result<ImageMetadata>::failure(color_type.status());
}
return pp::foundation::Result<ImageMetadata>::success(ImageMetadata {
.width = width,
.height = height,
.bit_depth = bit_depth,
.components = component_count(color_type.value()),
.color_type = color_type.value(),
});
}
const char* image_color_type_name(ImageColorType color_type) noexcept
{
switch (color_type) {
case ImageColorType::grayscale:
return "grayscale";
case ImageColorType::rgb:
return "rgb";
case ImageColorType::indexed:
return "indexed";
case ImageColorType::grayscale_alpha:
return "grayscale_alpha";
case ImageColorType::rgba:
return "rgba";
}
return "unknown";
}
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
namespace pp::assets {
constexpr std::uint32_t max_image_dimension = 262144;
enum class ImageColorType : std::uint8_t {
grayscale,
rgb,
indexed,
grayscale_alpha,
rgba,
};
struct ImageMetadata {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint8_t bit_depth = 0;
std::uint8_t components = 0;
ImageColorType color_type = ImageColorType::rgba;
};
[[nodiscard]] pp::foundation::Result<ImageMetadata> parse_png_metadata(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] const char* image_color_type_name(ImageColorType color_type) noexcept;
}

159
src/assets/image_pixels.cpp Normal file
View File

@@ -0,0 +1,159 @@
#include "assets/image_pixels.h"
#include "assets/image_metadata.h"
#include <limits>
#include <utility>
#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#define STB_IMAGE_WRITE_STATIC
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb/stb_image_write.h>
namespace pp::assets {
namespace {
[[nodiscard]] pp::foundation::Result<std::size_t> rgba_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
const auto pixels = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
constexpr auto channels = 4ULL;
if (pixels > std::numeric_limits<std::uint64_t>::max() / channels) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("RGBA byte size overflows"));
}
const auto bytes = pixels * channels;
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory"));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
void append_png_bytes(void* context, void* data, int size)
{
if (context == nullptr || data == nullptr || size <= 0) {
return;
}
auto* bytes = static_cast<std::vector<std::byte>*>(context);
const auto* begin = static_cast<const std::byte*>(data);
bytes->insert(bytes->end(), begin, begin + size);
}
}
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
{
const auto metadata = parse_png_metadata(bytes);
if (!metadata) {
return pp::foundation::Result<Rgba8Image>::failure(metadata.status());
}
if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<Rgba8Image>::failure(
pp::foundation::Status::out_of_range("PNG payload is too large for the decoder"));
}
int width = 0;
int height = 0;
int source_components = 0;
auto* decoded = stbi_load_from_memory(
reinterpret_cast<const stbi_uc*>(bytes.data()),
static_cast<int>(bytes.size()),
&width,
&height,
&source_components,
4);
if (decoded == nullptr) {
return pp::foundation::Result<Rgba8Image>::failure(
pp::foundation::Status::invalid_argument("PNG payload could not be decoded"));
}
const auto cleanup = [decoded]() noexcept {
stbi_image_free(decoded);
};
if (width <= 0 || height <= 0
|| static_cast<std::uint32_t>(width) != metadata.value().width
|| static_cast<std::uint32_t>(height) != metadata.value().height) {
cleanup();
return pp::foundation::Result<Rgba8Image>::failure(
pp::foundation::Status::invalid_argument("decoded PNG dimensions are inconsistent"));
}
const auto byte_count = rgba_byte_size(metadata.value().width, metadata.value().height);
if (!byte_count) {
cleanup();
return pp::foundation::Result<Rgba8Image>::failure(byte_count.status());
}
Rgba8Image image {
.width = metadata.value().width,
.height = metadata.value().height,
.pixels = {},
};
image.pixels.assign(decoded, decoded + byte_count.value());
cleanup();
return pp::foundation::Result<Rgba8Image>::success(std::move(image));
}
pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
std::uint32_t width,
std::uint32_t height,
std::span<const std::uint8_t> pixels)
{
if (width == 0 || height == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PNG dimensions must be greater than zero"));
}
if (width > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
|| height > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PNG dimensions exceed encoder limits"));
}
const auto byte_count = rgba_byte_size(width, height);
if (!byte_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(byte_count.status());
}
if (pixels.size() != byte_count.value()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("RGBA pixel payload size does not match dimensions"));
}
const auto stride = static_cast<std::uint64_t>(width) * 4ULL;
if (stride > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PNG row stride exceeds encoder limits"));
}
std::vector<std::byte> encoded;
const auto result = stbi_write_png_to_func(
append_png_bytes,
&encoded,
static_cast<int>(width),
static_cast<int>(height),
4,
pixels.data(),
static_cast<int>(stride));
if (result == 0 || encoded.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("RGBA pixels could not be encoded as PNG"));
}
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(encoded));
}
}

26
src/assets/image_pixels.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace pp::assets {
struct Rgba8Image {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::vector<std::uint8_t> pixels;
};
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
std::span<const std::byte> bytes);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
std::uint32_t width,
std::uint32_t height,
std::span<const std::uint8_t> pixels);
}

888
src/assets/ppi_header.cpp Normal file
View File

@@ -0,0 +1,888 @@
#include "assets/ppi_header.h"
#include "assets/image_metadata.h"
#include "foundation/binary_stream.h"
#include <array>
#include <bit>
#include <cmath>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
namespace pp::assets {
namespace {
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
{
return reader.read_u32_le();
}
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_positive_i32(
pp::foundation::ByteReader& reader,
const char* message) noexcept
{
const auto value = reader.read_u32_le();
if (!value) {
return value;
}
if (value.value() > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max())) {
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::out_of_range(message));
}
return value;
}
[[nodiscard]] pp::foundation::Result<float> read_f32(pp::foundation::ByteReader& reader) noexcept
{
const auto bits = reader.read_u32_le();
if (!bits) {
return pp::foundation::Result<float>::failure(bits.status());
}
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
}
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
{
bytes.push_back(static_cast<std::byte>(value & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
}
void append_f32(std::vector<std::byte>& bytes, float value)
{
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
}
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
{
for (const auto ch : value) {
bytes.push_back(static_cast<std::byte>(ch));
}
}
[[nodiscard]] pp::foundation::Status skip_bytes(
pp::foundation::ByteReader& reader,
std::size_t bytes) noexcept
{
const auto skipped = reader.read_bytes(bytes);
if (!skipped) {
return skipped.status();
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept
{
if (width == 0 || height == 0) {
return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero");
}
if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) {
return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit");
}
return pp::foundation::Status::success();
}
[[nodiscard]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count)
{
if (layer_count == 1U) {
return std::string(base_name);
}
std::string name(base_name);
name.push_back(' ');
name += std::to_string(layer_index + 1U);
return name;
}
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
{
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
if (next > max_ppi_face_payload_bytes) {
return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit");
}
summary.compressed_face_bytes = next;
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<ImageMetadata> validate_face_png_payload(
std::span<const std::byte> payload,
std::uint32_t width,
std::uint32_t height) noexcept
{
const auto metadata = parse_png_metadata(payload);
if (!metadata) {
return pp::foundation::Result<ImageMetadata>::failure(metadata.status());
}
if (metadata.value().width != width || metadata.value().height != height) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box"));
}
if (metadata.value().bit_depth != 8U || metadata.value().components != 4U
|| metadata.value().color_type != ImageColorType::rgba) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
}
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
}
}
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
{
if (bytes.size() < ppi_header_size) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::out_of_range("PPI header is truncated"));
}
pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size));
const auto magic = reader.read_bytes(4);
if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' }
|| magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI header magic is invalid"));
}
PpiHeaderInfo info;
const auto doc_major = read_u32(reader);
const auto doc_minor = read_u32(reader);
const auto soft_major = read_u32(reader);
const auto soft_minor = read_u32(reader);
const auto soft_fix = read_u32(reader);
const auto soft_build = read_u32(reader);
const auto thumb_width = read_u32(reader);
const auto thumb_height = read_u32(reader);
const auto thumb_components = read_u32(reader);
if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build
|| !thumb_width || !thumb_height || !thumb_components) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::out_of_range("PPI header is truncated"));
}
info.document_version = { doc_major.value(), doc_minor.value() };
info.software_version = {
soft_major.value(),
soft_minor.value(),
soft_fix.value(),
soft_build.value(),
};
info.thumbnail = {
thumb_width.value(),
thumb_height.value(),
thumb_components.value(),
};
if (info.document_version.major != 0 || info.document_version.minor < 1) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI document version is unsupported"));
}
if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
}
return pp::foundation::Result<PpiHeaderInfo>::success(info);
}
pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept
{
if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
}
const auto width = static_cast<std::uint64_t>(thumbnail.width);
const auto height = static_cast<std::uint64_t>(thumbnail.height);
const auto components = static_cast<std::uint64_t>(thumbnail.components);
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto pixels = width * height;
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto bytes = pixels * components;
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory"));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<const std::byte> bytes) noexcept
{
const auto header = parse_ppi_header(bytes);
if (!header) {
return pp::foundation::Result<PpiProjectLayout>::failure(header.status());
}
const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail);
if (!thumbnail_bytes) {
return pp::foundation::Result<PpiProjectLayout>::failure(thumbnail_bytes.status());
}
if (thumbnail_bytes.value() > std::numeric_limits<std::size_t>::max() - ppi_header_size) {
return pp::foundation::Result<PpiProjectLayout>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto body_offset = ppi_header_size + thumbnail_bytes.value();
if (bytes.size() < body_offset) {
return pp::foundation::Result<PpiProjectLayout>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated"));
}
return pp::foundation::Result<PpiProjectLayout>::success(PpiProjectLayout {
.header = header.value(),
.thumbnail_offset = ppi_header_size,
.thumbnail_bytes = thumbnail_bytes.value(),
.body_offset = body_offset,
.body_bytes = bytes.size() - body_offset,
});
}
namespace {
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
PpiHeaderInfo header,
std::span<const std::byte> body,
PpiBodyIndex* index) noexcept
{
if (index != nullptr) {
index->summary = {};
index->layers.clear();
}
pp::foundation::ByteReader reader(body);
const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range");
const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range");
const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range");
if (!width || !height || !layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
!width ? width.status() : (!height ? height.status() : layer_count.status()));
}
const auto canvas_status = validate_canvas_size(width.value(), height.value());
if (!canvas_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(canvas_status);
}
if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
PpiBodySummary summary {
.width = width.value(),
.height = height.value(),
.layer_count = layer_count.value(),
.declared_frame_count = 1,
};
std::vector<bool> seen_orders;
if (index != nullptr) {
index->layers.resize(summary.layer_count);
seen_orders.assign(summary.layer_count, false);
}
if (header.document_version.minor >= 3U) {
const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range");
if (!declared_frames) {
return pp::foundation::Result<PpiBodySummary>::failure(declared_frames.status());
}
if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range"));
}
summary.declared_frame_count = declared_frames.value();
}
for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) {
const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range");
const auto opacity = read_f32(reader);
const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range");
if (!order || !opacity || !name_length) {
return pp::foundation::Result<PpiBodySummary>::failure(
!order ? order.status() : (!opacity ? opacity.status() : name_length.status()));
}
if (order.value() >= summary.layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
}
if (index != nullptr) {
if (seen_orders[order.value()]) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer order is duplicated"));
}
seen_orders[order.value()] = true;
}
if (!std::isfinite(opacity.value())) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
}
if (name_length.value() > max_ppi_layer_name_length) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
}
const auto name_bytes = reader.read_bytes(name_length.value());
if (!name_bytes) {
return pp::foundation::Result<PpiBodySummary>::failure(name_bytes.status());
}
PpiLayerSummary layer_summary;
if (index != nullptr) {
layer_summary.stored_order = order.value();
layer_summary.opacity = opacity.value();
layer_summary.name.reserve(name_bytes.value().size());
for (const auto byte : name_bytes.value()) {
layer_summary.name.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
}
}
if (header.document_version.minor >= 2U) {
const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range");
const auto alpha_locked = reader.read_u8();
const auto visible = reader.read_u8();
if (!blend_mode || !alpha_locked || !visible) {
return pp::foundation::Result<PpiBodySummary>::failure(
!blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status()));
}
if (alpha_locked.value() > 1U || visible.value() > 1U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
}
if (blend_mode.value() > 4U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (index != nullptr) {
layer_summary.blend_mode = blend_mode.value();
layer_summary.alpha_locked = alpha_locked.value() != 0U;
layer_summary.visible = visible.value() != 0U;
}
}
std::uint32_t layer_frames = 1;
if (header.document_version.minor >= 3U) {
const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range");
if (!frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(frame_count.status());
}
if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
}
layer_frames = frame_count.value();
}
if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit"));
}
summary.total_layer_frames += layer_frames;
if (index != nullptr) {
layer_summary.frames.resize(layer_frames);
}
for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) {
if (header.document_version.minor >= 3U) {
const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range");
if (!duration) {
return pp::foundation::Result<PpiBodySummary>::failure(duration.status());
}
if (duration.value() == 0) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
}
if (index != nullptr) {
layer_summary.frames[frame_index].duration_ms = duration.value();
}
}
for (std::uint32_t face = 0; face < 6U; ++face) {
const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range");
if (!has_data) {
return pp::foundation::Result<PpiBodySummary>::failure(has_data.status());
}
if (has_data.value() > 1U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI face data flag is invalid"));
}
if (has_data.value() == 0U) {
continue;
}
++summary.dirty_face_count;
const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range");
if (!x0 || !y0 || !x1 || !y1 || !data_size) {
return pp::foundation::Result<PpiBodySummary>::failure(
!x0 ? x0.status()
: (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status()))));
}
if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width
|| y1.value() > summary.height) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas"));
}
if (data_size.value() == 0U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty"));
}
const auto byte_status = add_payload_bytes(summary, data_size.value());
if (!byte_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(byte_status);
}
const auto payload_offset = reader.position();
const auto payload = reader.read_bytes(data_size.value());
if (!payload) {
return pp::foundation::Result<PpiBodySummary>::failure(payload.status());
}
const auto png_metadata = validate_face_png_payload(
payload.value(),
x1.value() - x0.value(),
y1.value() - y0.value());
if (!png_metadata) {
return pp::foundation::Result<PpiBodySummary>::failure(png_metadata.status());
}
++summary.rgba_face_payload_count;
if (index != nullptr) {
layer_summary.frames[frame_index].faces[face] = PpiFacePayloadSummary {
.has_data = true,
.x0 = x0.value(),
.y0 = y0.value(),
.x1 = x1.value(),
.y1 = y1.value(),
.body_payload_offset = static_cast<std::uint32_t>(payload_offset),
.payload_bytes = data_size.value(),
.png_width = png_metadata.value().width,
.png_height = png_metadata.value().height,
};
}
}
}
if (index != nullptr) {
index->layers[order.value()] = std::move(layer_summary);
}
}
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames"));
}
if (header.document_version.minor >= 4U) {
const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range");
if (!info_bytes) {
return pp::foundation::Result<PpiBodySummary>::failure(info_bytes.status());
}
summary.info_bytes = info_bytes.value();
const auto info_status = skip_bytes(reader, summary.info_bytes);
if (!info_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(info_status);
}
}
if (!reader.empty()) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
}
if (index != nullptr) {
index->summary = summary;
}
return pp::foundation::Result<PpiBodySummary>::success(summary);
}
}
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
PpiHeaderInfo header,
std::span<const std::byte> body) noexcept
{
return parse_ppi_body_impl(header, body, nullptr);
}
pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
PpiHeaderInfo header,
std::span<const std::byte> body)
{
PpiBodyIndex index;
const auto summary = parse_ppi_body_impl(header, body, &index);
if (!summary) {
return pp::foundation::Result<PpiBodyIndex>::failure(summary.status());
}
return pp::foundation::Result<PpiBodyIndex>::success(std::move(index));
}
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
{
const auto layout = parse_ppi_project_layout(bytes);
if (!layout) {
return pp::foundation::Result<PpiProjectSummary>::failure(layout.status());
}
const auto body = parse_ppi_body_summary(
layout.value().header,
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
if (!body) {
return pp::foundation::Result<PpiProjectSummary>::failure(body.status());
}
return pp::foundation::Result<PpiProjectSummary>::success(PpiProjectSummary {
.layout = layout.value(),
.body = body.value(),
});
}
pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const std::byte> bytes)
{
const auto layout = parse_ppi_project_layout(bytes);
if (!layout) {
return pp::foundation::Result<PpiProjectIndex>::failure(layout.status());
}
const auto body = parse_ppi_body_index(
layout.value().header,
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
if (!body) {
return pp::foundation::Result<PpiProjectIndex>::failure(body.status());
}
return pp::foundation::Result<PpiProjectIndex>::success(PpiProjectIndex {
.layout = layout.value(),
.body = body.value(),
});
}
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
{
auto project = parse_ppi_project_index(bytes);
if (!project) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(project.status());
}
PpiDecodedProjectImages decoded {
.project = project.value(),
.faces = {},
};
decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count);
const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes);
for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) {
const auto& layer = decoded.project.body.layers[layer_index];
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
const auto& frame = layer.frames[frame_index];
for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) {
const auto& face = frame.faces[face_index];
if (!face.has_data) {
continue;
}
if (face.body_payload_offset > body.size()
|| face.payload_bytes > body.size() - face.body_payload_offset) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
pp::foundation::Status::out_of_range("PPI face payload range is outside the body"));
}
const auto image = decode_png_rgba8(
body.subspan(face.body_payload_offset, face.payload_bytes));
if (!image) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(image.status());
}
if (image.value().width != face.png_width || image.value().height != face.png_height) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
}
decoded.faces.push_back(PpiDecodedFacePayload {
.layer_index = static_cast<std::uint32_t>(layer_index),
.frame_index = static_cast<std::uint32_t>(frame_index),
.face_index = static_cast<std::uint32_t>(face_index),
.descriptor = face,
.image = image.value(),
});
}
}
}
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
}
pp::foundation::Result<std::vector<std::byte>> create_ppi_project(PpiProjectConfig config)
{
const auto canvas_status = validate_canvas_size(config.width, config.height);
if (!canvas_status.ok()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(canvas_status);
}
if (config.layers.empty() || config.layers.size() > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
std::uint32_t total_frame_count = 0;
std::vector<std::size_t> layer_frame_offsets;
layer_frame_offsets.reserve(config.layers.size());
for (const auto& layer : config.layers) {
if (layer.name.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer name must not be empty"));
}
if (layer.name.size() > max_ppi_layer_name_length) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
}
if (!std::isfinite(layer.metadata.opacity)) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
}
if (layer.metadata.blend_mode > 4U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (layer.frames.empty() || layer.frames.size() > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
}
if (layer.frames.size() > max_ppi_frame_count - total_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range"));
}
layer_frame_offsets.push_back(total_frame_count);
total_frame_count += static_cast<std::uint32_t>(layer.frames.size());
for (const auto& frame : layer.frames) {
if (frame.duration_ms == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
}
}
}
std::vector<std::array<bool, 6>> seen_faces(total_frame_count);
std::uint64_t total_payload_bytes = 0;
for (const auto& face : config.dirty_faces) {
if (face.layer_index >= config.layers.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list"));
}
if (face.frame_index >= config.layers[face.layer_index].frames.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list"));
}
if (face.face_index >= 6U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list"));
}
const auto slot_index = layer_frame_offsets[face.layer_index] + static_cast<std::size_t>(face.frame_index);
if (seen_faces[slot_index][face.face_index]) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated"));
}
seen_faces[slot_index][face.face_index] = true;
if (face.width == 0 || face.height == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face dimensions must be greater than zero"));
}
if (face.x > config.width || face.width > config.width - face.x
|| face.y > config.height || face.height > config.height - face.y) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas"));
}
if (face.png_rgba8.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty"));
}
if (face.png_rgba8.size() > static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payload is too large"));
}
const auto next_payload_bytes = total_payload_bytes + face.png_rgba8.size();
if (next_payload_bytes > max_ppi_face_payload_bytes) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payloads exceed the configured limit"));
}
total_payload_bytes = next_payload_bytes;
const auto metadata = validate_face_png_payload(face.png_rgba8, face.width, face.height);
if (!metadata) {
return pp::foundation::Result<std::vector<std::byte>>::failure(metadata.status());
}
}
std::vector<std::byte> bytes {
std::byte { 'P' },
std::byte { 'P' },
std::byte { 'I' },
std::byte { 0 },
};
append_u32(bytes, 0);
append_u32(bytes, 4);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 128);
append_u32(bytes, 128);
append_u32(bytes, 4);
constexpr std::size_t thumbnail_bytes = 128U * 128U * 4U;
bytes.resize(ppi_header_size + thumbnail_bytes, std::byte { 0 });
append_u32(bytes, config.width);
append_u32(bytes, config.height);
append_u32(bytes, static_cast<std::uint32_t>(config.layers.size()));
append_u32(bytes, total_frame_count);
for (std::uint32_t layer = 0; layer < config.layers.size(); ++layer) {
const auto& layer_config = config.layers[layer];
append_u32(bytes, layer);
append_f32(bytes, layer_config.metadata.opacity);
append_u32(bytes, static_cast<std::uint32_t>(layer_config.name.size()));
append_ascii(bytes, layer_config.name);
append_u32(bytes, layer_config.metadata.blend_mode);
bytes.push_back(layer_config.metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 });
bytes.push_back(layer_config.metadata.visible ? std::byte { 1 } : std::byte { 0 });
append_u32(bytes, static_cast<std::uint32_t>(layer_config.frames.size()));
for (std::uint32_t frame = 0; frame < layer_config.frames.size(); ++frame) {
append_u32(bytes, layer_config.frames[frame].duration_ms);
for (std::uint32_t face = 0; face < 6U; ++face) {
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
for (const auto& candidate : config.dirty_faces) {
if (candidate.layer_index == layer && candidate.frame_index == frame
&& candidate.face_index == face) {
dirty_face = &candidate;
break;
}
}
if (dirty_face == nullptr) {
append_u32(bytes, 0);
continue;
}
append_u32(bytes, 1);
append_u32(bytes, dirty_face->x);
append_u32(bytes, dirty_face->y);
append_u32(bytes, dirty_face->x + dirty_face->width);
append_u32(bytes, dirty_face->y + dirty_face->height);
append_u32(bytes, static_cast<std::uint32_t>(dirty_face->png_rgba8.size()));
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
}
}
}
append_u32(bytes, 0);
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(bytes));
}
pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMinimalProjectConfig config)
{
if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
if (config.frame_count == 0 || config.frame_count > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI frame count is outside the configured range"));
}
std::vector<std::string> names;
names.reserve(config.layer_count);
std::vector<std::vector<PpiFrameConfig>> frame_lists;
frame_lists.reserve(config.layer_count);
std::vector<PpiLayerConfig> layers;
layers.reserve(config.layer_count);
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
names.push_back(generated_layer_name(config.layer_name, layer, config.layer_count));
auto& frames = frame_lists.emplace_back();
frames.assign(config.frame_count, PpiFrameConfig { .duration_ms = config.frame_duration_ms });
layers.push_back(PpiLayerConfig {
.name = names.back(),
.metadata = config.layer_metadata,
.frames = std::span<const PpiFrameConfig>(frames.data(), frames.size()),
});
}
return create_ppi_project(PpiProjectConfig {
.width = config.width,
.height = config.height,
.layers = std::span<const PpiLayerConfig>(layers.data(), layers.size()),
.dirty_faces = config.dirty_faces,
});
}
}

199
src/assets/ppi_header.h Normal file
View File

@@ -0,0 +1,199 @@
#pragma once
#include "assets/image_pixels.h"
#include "foundation/result.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace pp::assets {
constexpr std::size_t ppi_header_size = 40;
constexpr std::uint32_t max_ppi_canvas_dimension = 131072;
constexpr std::uint32_t max_ppi_layer_count = 1024;
constexpr std::uint32_t max_ppi_frame_count = 100000;
constexpr std::size_t max_ppi_layer_name_length = 128;
constexpr std::uint64_t max_ppi_face_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
struct PpiVersion {
std::uint32_t major = 0;
std::uint32_t minor = 0;
};
struct PpiSoftwareVersion {
std::uint32_t major = 0;
std::uint32_t minor = 0;
std::uint32_t fix = 0;
std::uint32_t build = 0;
};
struct PpiThumbnailInfo {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t components = 0;
};
struct PpiHeaderInfo {
PpiVersion document_version;
PpiSoftwareVersion software_version;
PpiThumbnailInfo thumbnail;
};
struct PpiProjectLayout {
PpiHeaderInfo header;
std::size_t thumbnail_offset = 0;
std::size_t thumbnail_bytes = 0;
std::size_t body_offset = 0;
std::size_t body_bytes = 0;
};
struct PpiBodySummary {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t layer_count = 0;
std::uint32_t declared_frame_count = 0;
std::uint32_t total_layer_frames = 0;
std::uint32_t dirty_face_count = 0;
std::uint32_t rgba_face_payload_count = 0;
std::uint64_t compressed_face_bytes = 0;
std::uint32_t info_bytes = 0;
};
struct PpiProjectSummary {
PpiProjectLayout layout;
PpiBodySummary body;
};
struct PpiFacePayloadSummary {
bool has_data = false;
std::uint32_t x0 = 0;
std::uint32_t y0 = 0;
std::uint32_t x1 = 0;
std::uint32_t y1 = 0;
std::uint32_t body_payload_offset = 0;
std::uint32_t payload_bytes = 0;
std::uint32_t png_width = 0;
std::uint32_t png_height = 0;
};
struct PpiFrameSummary {
std::uint32_t duration_ms = 100;
std::array<PpiFacePayloadSummary, 6> faces;
};
struct PpiLayerSummary {
std::uint32_t stored_order = 0;
std::string name;
float opacity = 1.0F;
std::uint32_t blend_mode = 0;
bool alpha_locked = false;
bool visible = true;
std::vector<PpiFrameSummary> frames;
};
struct PpiBodyIndex {
PpiBodySummary summary;
std::vector<PpiLayerSummary> layers;
};
struct PpiProjectIndex {
PpiProjectLayout layout;
PpiBodyIndex body;
};
struct PpiDecodedFacePayload {
std::uint32_t layer_index = 0;
std::uint32_t frame_index = 0;
std::uint32_t face_index = 0;
PpiFacePayloadSummary descriptor;
Rgba8Image image;
};
struct PpiDecodedProjectImages {
PpiProjectIndex project;
std::vector<PpiDecodedFacePayload> faces;
};
struct PpiDirtyFacePayloadConfig {
std::uint32_t layer_index = 0;
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::byte> png_rgba8;
};
struct PpiLayerMetadataConfig {
float opacity = 1.0F;
std::uint32_t blend_mode = 0;
bool alpha_locked = false;
bool visible = true;
};
struct PpiFrameConfig {
std::uint32_t duration_ms = 100;
};
struct PpiLayerConfig {
std::string_view name;
PpiLayerMetadataConfig metadata;
std::span<const PpiFrameConfig> frames;
};
struct PpiProjectConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::span<const PpiLayerConfig> layers;
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
};
struct PpiMinimalProjectConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::string layer_name;
PpiLayerMetadataConfig layer_metadata;
std::uint32_t layer_count = 1;
std::uint32_t frame_count = 1;
std::uint32_t frame_duration_ms = 100;
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
};
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept;
[[nodiscard]] pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
PpiHeaderInfo header,
std::span<const std::byte> body) noexcept;
[[nodiscard]] pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
PpiHeaderInfo header,
std::span<const std::byte> body);
[[nodiscard]] pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(
std::span<const std::byte> bytes);
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
std::span<const std::byte> bytes);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_ppi_project(
PpiProjectConfig config);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(
PpiMinimalProjectConfig config);
}

View File

@@ -0,0 +1,183 @@
#include "assets/settings_document.h"
#include <algorithm>
#include <cctype>
#include <cmath>
namespace pp::assets {
namespace {
[[nodiscard]] bool is_valid_key_char(char value) noexcept
{
const auto ch = static_cast<unsigned char>(value);
return std::isalnum(ch) != 0 || value == '_' || value == '-' || value == '.';
}
}
std::size_t SettingsDocument::size() const noexcept
{
return entries_.size();
}
bool SettingsDocument::empty() const noexcept
{
return entries_.empty();
}
bool SettingsDocument::has(std::string_view key) const noexcept
{
return find_entry(key) != entries_.end();
}
const std::vector<SettingsEntry>& SettingsDocument::entries() const noexcept
{
return entries_;
}
pp::foundation::Status SettingsDocument::set(std::string_view key, SettingsValue value)
{
const auto key_status = validate_settings_key(key);
if (!key_status.ok()) {
return key_status;
}
const auto value_status = validate_settings_value(value);
if (!value_status.ok()) {
return value_status;
}
auto found = find_entry(key);
if (found != entries_.end()) {
found->value = value;
return pp::foundation::Status::success();
}
if (entries_.size() >= max_settings_entries) {
return pp::foundation::Status::out_of_range("settings entry count exceeds the configured limit");
}
entries_.push_back(SettingsEntry {
.key = std::string(key),
.value = value,
});
return pp::foundation::Status::success();
}
pp::foundation::Result<SettingsValue> SettingsDocument::get(std::string_view key) const
{
const auto key_status = validate_settings_key(key);
if (!key_status.ok()) {
return pp::foundation::Result<SettingsValue>::failure(key_status);
}
const auto found = find_entry(key);
if (found == entries_.end()) {
return pp::foundation::Result<SettingsValue>::failure(
pp::foundation::Status::out_of_range("settings key was not found"));
}
return pp::foundation::Result<SettingsValue>::success(found->value);
}
pp::foundation::Status SettingsDocument::unset(std::string_view key) noexcept
{
const auto key_status = validate_settings_key(key);
if (!key_status.ok()) {
return key_status;
}
const auto found = find_entry(key);
if (found == entries_.end()) {
return pp::foundation::Status::out_of_range("settings key was not found");
}
entries_.erase(found);
return pp::foundation::Status::success();
}
void SettingsDocument::clear() noexcept
{
entries_.clear();
}
std::vector<SettingsEntry>::iterator SettingsDocument::find_entry(std::string_view key) noexcept
{
return std::find_if(
entries_.begin(),
entries_.end(),
[key](const SettingsEntry& entry) {
return entry.key == key;
});
}
std::vector<SettingsEntry>::const_iterator SettingsDocument::find_entry(std::string_view key) const noexcept
{
return std::find_if(
entries_.begin(),
entries_.end(),
[key](const SettingsEntry& entry) {
return entry.key == key;
});
}
pp::foundation::Status validate_settings_key(std::string_view key) noexcept
{
if (key.empty()) {
return pp::foundation::Status::invalid_argument("settings key must not be empty");
}
if (key.size() > max_settings_key_length) {
return pp::foundation::Status::out_of_range("settings key length exceeds the configured limit");
}
if (key.front() == '.' || key.back() == '.') {
return pp::foundation::Status::invalid_argument("settings key must not start or end with a dot");
}
for (const auto ch : key) {
if (!is_valid_key_char(ch)) {
return pp::foundation::Status::invalid_argument("settings key contains an unsupported character");
}
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept
{
if (const auto* string_value = std::get_if<std::string>(&value)) {
if (string_value->size() > max_settings_string_length) {
return pp::foundation::Status::out_of_range("settings string length exceeds the configured limit");
}
}
if (const auto* double_value = std::get_if<double>(&value)) {
if (!std::isfinite(*double_value)) {
return pp::foundation::Status::invalid_argument("settings floating point value must be finite");
}
}
return pp::foundation::Status::success();
}
const char* settings_value_type_name(const SettingsValue& value) noexcept
{
if (std::holds_alternative<bool>(value)) {
return "bool";
}
if (std::holds_alternative<std::int64_t>(value)) {
return "int64";
}
if (std::holds_alternative<double>(value)) {
return "double";
}
if (std::holds_alternative<std::string>(value)) {
return "string";
}
return "unknown";
}
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace pp::assets {
constexpr std::size_t max_settings_entries = 4096;
constexpr std::size_t max_settings_key_length = 128;
constexpr std::size_t max_settings_string_length = 4096;
using SettingsValue = std::variant<bool, std::int64_t, double, std::string>;
struct SettingsEntry {
std::string key;
SettingsValue value;
};
class SettingsDocument {
public:
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] bool has(std::string_view key) const noexcept;
[[nodiscard]] const std::vector<SettingsEntry>& entries() const noexcept;
[[nodiscard]] pp::foundation::Status set(std::string_view key, SettingsValue value);
[[nodiscard]] pp::foundation::Result<SettingsValue> get(std::string_view key) const;
[[nodiscard]] pp::foundation::Status unset(std::string_view key) noexcept;
void clear() noexcept;
private:
[[nodiscard]] std::vector<SettingsEntry>::iterator find_entry(std::string_view key) noexcept;
[[nodiscard]] std::vector<SettingsEntry>::const_iterator find_entry(std::string_view key) const noexcept;
std::vector<SettingsEntry> entries_;
};
[[nodiscard]] pp::foundation::Status validate_settings_key(std::string_view key) noexcept;
[[nodiscard]] pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept;
[[nodiscard]] const char* settings_value_type_name(const SettingsValue& value) noexcept;
}

File diff suppressed because it is too large Load Diff

View File

@@ -205,7 +205,7 @@ public:
void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
// return rect {origin, size}
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P);
glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P, bool copy_stroke_destination);
std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
void stroke_draw();
void stroke_end();

View File

@@ -4,6 +4,7 @@
#include "canvas.h"
#include "canvas_actions.h"
#include "node_panel_layer.h"
#include "renderer_gl/opengl_capabilities.h"
void ActionStroke::undo()
{
@@ -36,8 +37,21 @@ void ActionStroke::undo()
{
App::I->render_task([&]
{
const auto texture_target = pp::renderer::gl::texture_2d_target();
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).bindTexture();
glTexSubImage2D(GL_TEXTURE_2D, 0, (int)m_box[i].x, (int)m_box[i].y, (int)box_sz.x, (int)box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, m_image[i].get());
glTexSubImage2D(
texture_target,
0,
(int)m_box[i].x,
(int)m_box[i].y,
(int)box_sz.x,
(int)box_sz.y,
pixel_format,
component_type,
m_image[i].get());
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).unbindTexture();
});
}
@@ -76,11 +90,22 @@ Action* ActionStroke::get_redo()
glm::vec2 box_sz = zw(box) - xy(box);
if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= layer->w && box_sz.y <= layer->h)
{
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
action->m_image[i] = std::make_unique<uint8_t[]>(
static_cast<size_t>((int)box_sz.x) * static_cast<size_t>((int)box_sz.y) * 4U);
App::I->render_task([&]
{
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
layer->rtt(i, m_frame_idx).bindFramebuffer();
glReadPixels(box_or.x, box_or.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
(int)box_or.x,
(int)box_or.y,
(int)box_sz.x,
(int)box_sz.y,
pixel_format,
component_type,
action->m_image[i].get());
layer->rtt(i, m_frame_idx).unbindFramebuffer();
});
}

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "canvas_layer.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include "rtt.h"
uint32_t Layer::s_count = 0;
@@ -44,7 +45,7 @@ TextureCube Layer::gen_cube()
{
ret.bind();
rtt(i).bindFramebuffer();
glCopyTexSubImage2D(TextureCube::m_faces_map[i], 0, 0, 0, 0, 0, w, w);
glCopyTexSubImage2D(pp::renderer::gl::cube_face_texture_target(i), 0, 0, 0, 0, 0, w, w);
rtt(i).unbindFramebuffer();
});
}
@@ -70,7 +71,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
latlong.create(size.x * 4, size.y * 2);
ret.create(size.x * 4, size.y * 2);
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
latlong.bindFramebuffer();
@@ -78,8 +79,8 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
ShaderManager::use(kShader::Equirect);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
@@ -88,7 +89,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
Canvas::I->m_plane.draw_fill();
ret.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
latlong.unbindFramebuffer();
@@ -115,13 +116,13 @@ PBO Layer::gen_equirect_pbo(glm::ivec2 size /*= { 0, 0 }*/)
App::I->render_task([&]
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
latlong.bindFramebuffer();
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
ShaderManager::use(kShader::Equirect);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
@@ -458,7 +459,7 @@ void LayerFrame::clear(const glm::vec4& c)
{
// push clear color state
GLfloat cc[4];
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
glClearColor(c.r, c.g, c.b, c.a);
bool erase = (c.a == 0.f);
@@ -466,7 +467,7 @@ void LayerFrame::clear(const glm::vec4& c)
for (int i = 0; i < 6; i++)
{
m_rtt[i].bindFramebuffer();
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
m_rtt[i].unbindFramebuffer();
if (erase)
@@ -530,9 +531,11 @@ void LayerFrame::restore(const Snapshot& snap)
m_rtt[i].bindTexture();
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
m_dirty_box[i].x, m_dirty_box[i].y,
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE,
glTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0,
static_cast<int>(m_dirty_box[i].x), static_cast<int>(m_dirty_box[i].y),
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
snap.image[i].get());
m_rtt[i].unbindTexture();
LOG("restore face %d - %d bytes (%dx%d)", i,
@@ -560,8 +563,11 @@ LayerFrame::Snapshot LayerFrame::snapshot(std::array<glm::vec4, 6>* dirty_box /*
m_rtt[i].bindFramebuffer();
glm::vec2 box_sz = zw(snap.m_dirty_box[i]) - xy(snap.m_dirty_box[i]);
glReadPixels(snap.m_dirty_box[i].x, snap.m_dirty_box[i].y,
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, snap.image[i].get());
glReadPixels(static_cast<int>(snap.m_dirty_box[i].x), static_cast<int>(snap.m_dirty_box[i].y),
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
snap.image[i].get());
m_rtt[i].unbindFramebuffer();
}
});

View File

@@ -1,4 +1,7 @@
#include "pch.h"
#include <cstdint>
#include "log.h"
#include "canvas_modes.h"
#include "layout.h"
@@ -7,9 +10,19 @@
#include "node_canvas.h"
#include "app.h"
#include "util.h"
#include "renderer_gl/opengl_capabilities.h"
NodeCanvas* CanvasMode::node;
namespace {
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
}
void CanvasModeBasicCamera::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
{
switch (me->m_type)
@@ -292,7 +305,10 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
}
glReadPixels((pos.x / App::I->width) * fb_width,
((App::I->height - pos.y - 1) / App::I->height) * fb_height,
1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
1, 1,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
&pixel);
bool outline = glm::min(tip_scale.x, tip_scale.y) < 20 || m_resizing ? false : m_draw_outline;
ShaderManager::u_int(kShaderUniform::DrawOutline, outline);
ShaderManager::u_vec4(kShaderUniform::Col, outline ? glm::vec4(1.f - glm::vec3(pixel) / 255.f, 1.f) : tip_color);
@@ -303,15 +319,15 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
glm::eulerAngleZ(tip_angle) *
glm::scale(glm::vec3(tip_scale, 1))
);
bool blend = glIsEnabled(GL_BLEND);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
glEnable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
auto& tex = *brush->m_tip_texture;
tex.bind();
Canvas::I->m_sampler_brush.bind(0);
Canvas::I->m_plane.draw_fill();
tex.unbind();
if (!blend) glDisable(GL_BLEND);
if (!blend) glDisable(pp::renderer::gl::blend_state());
}
}
@@ -409,15 +425,15 @@ void CanvasModeLine::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, cons
glm::eulerAngleZ(tip_angle) *
glm::scale(glm::vec3(tip_scale, 1))
);
bool blend = glIsEnabled(GL_BLEND);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
glEnable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
auto& tex = *brush->m_tip_texture;
tex.bind();
Canvas::I->m_sampler_brush.bind(0);
Canvas::I->m_plane.draw_fill();
tex.unbind();
if (!blend) glDisable(GL_BLEND);
if (!blend) glDisable(pp::renderer::gl::blend_state());
}
}
@@ -704,8 +720,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
m_selection_cam = Canvas::I->get_camera();
//m_points2d = poly_intersect(poly_remove_duplicate(m_points2d), Canvas::I->face_to_shape2D(0));
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
//glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
// blending state intentionally left unchanged here.
glDisable(pp::renderer::gl::depth_test_state());
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
ShaderManager::u_vec4(kShaderUniform::Col,
@@ -783,8 +799,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
{
bool depth = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::depth_test_state());
if (m_points.size() > 3)
{
if (m_dragging)
@@ -803,7 +819,7 @@ void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
// m_shape.draw_stroke();
//}
}
if (depth) glEnable(GL_DEPTH_TEST);
if (depth) glEnable(pp::renderer::gl::depth_test_state());
}
@@ -840,7 +856,7 @@ void CanvasModeMaskLine::leave(kCanvasMode next)
if (!m_points.empty())
{
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
//glEnable(GL_BLEND);
// blending state intentionally left unchanged here.
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
ShaderManager::u_vec4(kShaderUniform::Col, {1, 1, 1, 1});
@@ -1248,7 +1264,9 @@ void CanvasModeTransform::enter(kCanvasMode prev)
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).bindFramebuffer();
m_tex[plane].create(bb_sz.x, bb_sz.y);
m_tex[plane].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
m_tex[plane].unbind();
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).unbindFramebuffer();
});
@@ -1307,15 +1325,22 @@ void CanvasModeTransform::enter(kCanvasMode prev)
App::I->render_task([&]
{
glViewport(0, 0, layer->w, layer->h);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
glDisable(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 0 });
layer->rtt(i).bindFramebuffer();
// copy framebuffer to action data
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
bb_min.x,
bb_min.y,
bb_sz.x,
bb_sz.y,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
action->m_image[i].get());
for (int j = 0; j < 6; j++)
m_shape[j].draw_fill();
layer->rtt(i).unbindFramebuffer();
@@ -1407,19 +1432,28 @@ void CanvasModeTransform::leave(kCanvasMode next)
{
layer->rtt(i).bindFramebuffer();
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
glDisable(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
glViewport(0, 0, layer->rtt(i).getWidth(), layer->rtt(i).getHeight());
// save fb content for history
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
bb_min.x,
bb_min.y,
bb_sz.x,
bb_sz.y,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
action->m_image[i].get());
// copy fb content to texture for blending
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
Canvas::I->m_tex2[i].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
// slot for m_tex
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
for (int j = 0; j < 6; j++)
{
ShaderManager::use(kShader::CompDraw);
@@ -1456,10 +1490,10 @@ void CanvasModeTransform::leave(kCanvasMode next)
void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
{
bool depth = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::depth_test_state());
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
for (int i = 0; i < 6; i++)
{
ShaderManager::use(kShader::Color);
@@ -1470,7 +1504,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * m_xform * m_xform_local);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex[i].bind();
Canvas::I->m_sampler_linear.bind(0);
m_shape[i].draw_fill();
@@ -1499,7 +1533,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
m_circle.draw_stroke();
}
if (depth) glEnable(GL_DEPTH_TEST);
if (depth) glEnable(pp::renderer::gl::depth_test_state());
}
void CanvasModeTransform::on_MouseEvent(MouseEvent* me, glm::vec2& loc)

915
src/document/document.cpp Normal file
View File

@@ -0,0 +1,915 @@
#include "document/document.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <string>
#include <utility>
namespace pp::document {
namespace {
[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept
{
if (config.width == 0 || config.height == 0) {
return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero");
}
if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) {
return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit");
}
if (config.layer_count == 0) {
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
}
if (config.layer_count > max_layer_count) {
return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit");
}
return pp::foundation::Status::success();
}
[[nodiscard]] std::string default_layer_name(std::size_t index)
{
return "Layer " + std::to_string(index + 1U);
}
[[nodiscard]] std::uint64_t frame_duration_sum(std::span<const AnimationFrame> frames) noexcept
{
std::uint64_t duration = 0;
for (const auto& frame : frames) {
duration += frame.duration_ms;
}
return duration;
}
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
{
if (name.empty()) {
return pp::foundation::Status::invalid_argument("layer name must not be empty");
}
if (name.size() > max_layer_name_length) {
return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_layer_opacity(float opacity) noexcept
{
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_blend_mode(pp::paint::BlendMode blend_mode) noexcept
{
switch (blend_mode) {
case pp::paint::BlendMode::normal:
case pp::paint::BlendMode::multiply:
case pp::paint::BlendMode::screen:
case pp::paint::BlendMode::color_dodge:
case pp::paint::BlendMode::overlay:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
}
[[nodiscard]] pp::foundation::Status validate_frame_duration(std::uint32_t duration_ms) noexcept
{
if (duration_ms < min_frame_duration_ms) {
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
{
if (index >= layer_count) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_frame_index(std::size_t index, std::size_t frame_count) noexcept
{
if (index >= frame_count) {
return pp::foundation::Status::out_of_range("frame index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
std::uint32_t width,
std::uint32_t height,
std::uint32_t components,
const char* dimensions_overflow_message,
const char* byte_size_overflow_message,
const char* payload_limit_message,
const char* addressable_memory_message) noexcept
{
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range(dimensions_overflow_message));
}
const auto pixels = width64 * height64;
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range(byte_size_overflow_message));
}
const auto bytes = pixels * components;
if (bytes > max_face_pixel_payload_bytes) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range(payload_limit_message));
}
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range(addressable_memory_message));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
rgba8_components,
"face pixel dimensions overflow",
"face pixel byte size overflows",
"face pixel payload exceeds the configured limit",
"face pixel payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Result<std::size_t> alpha8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
alpha8_components,
"selection mask dimensions overflow",
"selection mask byte size overflows",
"selection mask payload exceeds the configured limit",
"selection mask payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Status validate_face_pixels(
LayerFacePixels pixels,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
if (pixels.face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("cube face index is outside the document");
}
if (pixels.width == 0 || pixels.height == 0) {
return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
}
if (pixels.x > document_width || pixels.width > document_width - pixels.x
|| pixels.y > document_height || pixels.height > document_height - pixels.y) {
return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
}
const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
if (!expected_bytes) {
return expected_bytes.status();
}
if (pixels.rgba8.size() != expected_bytes.value()) {
return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_selection_mask(
SelectionMask mask,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
if (mask.face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
if (mask.width == 0 || mask.height == 0) {
return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero");
}
if (mask.x > document_width || mask.width > document_width - mask.x
|| mask.y > document_height || mask.height > document_height - mask.y) {
return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document");
}
const auto expected_bytes = alpha8_byte_size(mask.width, mask.height);
if (!expected_bytes) {
return expected_bytes.status();
}
if (mask.alpha8.size() != expected_bytes.value()) {
return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_frame_face_pixels(
std::span<const AnimationFrame> frames,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
for (const auto& frame : frames) {
std::array<bool, cube_face_count> seen_faces {};
for (const auto& pixels : frame.face_pixels) {
const auto pixels_status = validate_face_pixels(pixels, document_width, document_height);
if (!pixels_status.ok()) {
return pixels_status;
}
if (seen_faces[pixels.face_index]) {
return pp::foundation::Status::invalid_argument(
"snapshot contains duplicate face pixel payloads for a cube face");
}
seen_faces[pixels.face_index] = true;
}
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
{
const auto status = validate_config(config);
if (!status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(status);
}
CanvasDocument document;
document.width_ = config.width;
document.height_ = config.height;
document.frames_.push_back(AnimationFrame {});
document.layers_.reserve(config.layer_count);
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
document.layers_.push_back(Layer {
.name = default_layer_name(i),
.frames = document.frames_,
});
}
return pp::foundation::Result<CanvasDocument>::success(document);
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(DocumentSnapshotConfig config)
{
const auto status = validate_config(DocumentConfig {
.width = config.width,
.height = config.height,
.layer_count = static_cast<std::uint32_t>(config.layers.size()),
});
if (!status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(status);
}
if (config.frames.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("document must contain at least one frame"));
}
if (config.frames.size() > max_frame_count) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
}
CanvasDocument document;
document.width_ = config.width;
document.height_ = config.height;
document.layers_.reserve(config.layers.size());
for (const auto& layer_config : config.layers) {
const auto name_status = validate_layer_name(layer_config.name);
if (!name_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(name_status);
}
const auto opacity_status = validate_layer_opacity(layer_config.opacity);
if (!opacity_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(opacity_status);
}
const auto blend_status = validate_blend_mode(layer_config.blend_mode);
if (!blend_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(blend_status);
}
const auto layer_frames = layer_config.frames.empty() ? config.frames : layer_config.frames;
if (layer_frames.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("document layer must contain at least one frame"));
}
if (layer_frames.size() > max_frame_count) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
}
const auto face_pixels_status = validate_frame_face_pixels(layer_frames, config.width, config.height);
if (!face_pixels_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
}
for (const auto& frame_config : layer_frames) {
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
if (!duration_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
}
}
document.layers_.push_back(Layer {
.name = std::string(layer_config.name),
.visible = layer_config.visible,
.alpha_locked = layer_config.alpha_locked,
.opacity = layer_config.opacity,
.blend_mode = layer_config.blend_mode,
.frames = {},
});
document.layers_.back().frames.assign(layer_frames.begin(), layer_frames.end());
}
document.frames_.reserve(config.frames.size());
for (const auto& frame_config : config.frames) {
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
if (!duration_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
}
const auto face_pixels_status = validate_frame_face_pixels(
std::span<const AnimationFrame>(&frame_config, 1),
config.width,
config.height);
if (!face_pixels_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
}
document.frames_.push_back(frame_config);
}
std::array<bool, cube_face_count> seen_selection_masks {};
for (const auto& mask : config.selection_masks) {
const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
if (!mask_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(mask_status);
}
if (seen_selection_masks[mask.face_index]) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument(
"snapshot contains duplicate selection masks for a cube face"));
}
seen_selection_masks[mask.face_index] = true;
document.selection_masks_.push_back(mask);
}
return pp::foundation::Result<CanvasDocument>::success(document);
}
std::uint32_t CanvasDocument::width() const noexcept
{
return width_;
}
std::uint32_t CanvasDocument::height() const noexcept
{
return height_;
}
std::size_t CanvasDocument::active_layer_index() const noexcept
{
return active_layer_index_;
}
std::size_t CanvasDocument::active_frame_index() const noexcept
{
return active_frame_index_;
}
std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
{
std::uint64_t duration = frame_duration_sum(frames_);
for (const auto& layer : layers_) {
duration = std::max(duration, frame_duration_sum(layer.frames));
}
return duration;
}
pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_ms(std::size_t index) const noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return pp::foundation::Result<std::uint64_t>::failure(index_status);
}
return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
}
std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
{
std::size_t count = 0;
for (const auto& layer : layers_) {
for (const auto& frame : layer.frames) {
count += frame.face_pixels.size();
}
}
return count;
}
std::size_t CanvasDocument::selection_mask_payload_count() const noexcept
{
return selection_masks_.size();
}
std::span<const Layer> CanvasDocument::layers() const noexcept
{
return layers_;
}
std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
{
return frames_;
}
std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
{
return selection_masks_;
}
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
{
if (layers_.size() >= max_layer_count) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"));
}
Layer layer;
if (name.empty()) {
layer.name = default_layer_name(layers_.size());
} else {
const auto name_status = validate_layer_name(name);
if (!name_status.ok()) {
return pp::foundation::Result<std::size_t>::failure(name_status);
}
layer.name = std::string(name);
}
layer.frames = frames_;
layers_.push_back(layer);
active_layer_index_ = layers_.size() - 1U;
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
}
pp::foundation::Status CanvasDocument::remove_layer(std::size_t index)
{
if (index >= layers_.size()) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
if (layers_.size() == 1U) {
return pp::foundation::Status::invalid_argument("document must keep at least one layer");
}
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(index));
if (active_layer_index_ >= layers_.size()) {
active_layer_index_ = layers_.size() - 1U;
} else if (active_layer_index_ > index) {
--active_layer_index_;
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to)
{
if (from >= layers_.size() || to >= layers_.size()) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
if (from == to) {
return pp::foundation::Status::success();
}
auto layer = layers_[from];
layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(from));
layers_.insert(layers_.begin() + static_cast<std::ptrdiff_t>(to), layer);
if (active_layer_index_ == from) {
active_layer_index_ = to;
} else if (from < active_layer_index_ && active_layer_index_ <= to) {
--active_layer_index_;
} else if (to <= active_layer_index_ && active_layer_index_ < from) {
++active_layer_index_;
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
active_layer_index_ = index;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
const auto name_status = validate_layer_name(name);
if (!name_status.ok()) {
return name_status;
}
layers_[index].name = std::string(name);
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
layers_[index].visible = visible;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
layers_[index].alpha_locked = alpha_locked;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
const auto opacity_status = validate_layer_opacity(opacity);
if (!opacity_status.ok()) {
return opacity_status;
}
layers_[index].opacity = opacity;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
const auto blend_status = validate_blend_mode(blend_mode);
if (!blend_status.ok()) {
return blend_status;
}
layers_[index].blend_mode = blend_mode;
return pp::foundation::Status::success();
}
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
{
if (frames_.size() >= max_frame_count) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
}
const auto duration_status = validate_frame_duration(duration_ms);
if (!duration_status.ok()) {
return pp::foundation::Result<std::size_t>::failure(
duration_status);
}
frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
for (auto& layer : layers_) {
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
}
active_frame_index_ = frames_.size() - 1U;
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
}
pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
{
const auto index_status = validate_frame_index(index, frames_.size());
if (!index_status.ok()) {
return pp::foundation::Result<std::size_t>::failure(
index_status);
}
if (frames_.size() >= max_frame_count) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
}
const auto insert_at = index + 1U;
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
for (auto& layer : layers_) {
if (index < layer.frames.size()) {
layer.frames.insert(
layer.frames.begin() + static_cast<std::ptrdiff_t>(insert_at),
layer.frames[index]);
}
}
active_frame_index_ = insert_at;
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
}
pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
{
const auto index_status = validate_frame_index(index, frames_.size());
if (!index_status.ok()) {
return index_status;
}
if (frames_.size() == 1U) {
return pp::foundation::Status::invalid_argument("document must keep at least one frame");
}
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
for (auto& layer : layers_) {
if (index < layer.frames.size() && layer.frames.size() > 1U) {
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(index));
}
}
if (active_frame_index_ >= frames_.size()) {
active_frame_index_ = frames_.size() - 1U;
} else if (active_frame_index_ > index) {
--active_frame_index_;
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t to)
{
if (from >= frames_.size() || to >= frames_.size()) {
return pp::foundation::Status::out_of_range("frame index is outside the document");
}
if (from == to) {
return pp::foundation::Status::success();
}
const auto frame = frames_[from];
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
for (auto& layer : layers_) {
if (from < layer.frames.size() && to < layer.frames.size()) {
const auto layer_frame = layer.frames[from];
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(from));
layer.frames.insert(layer.frames.begin() + static_cast<std::ptrdiff_t>(to), layer_frame);
}
}
if (active_frame_index_ == from) {
active_frame_index_ = to;
} else if (from < active_frame_index_ && active_frame_index_ <= to) {
--active_frame_index_;
} else if (to <= active_frame_index_ && active_frame_index_ < from) {
++active_frame_index_;
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept
{
const auto index_status = validate_frame_index(index, frames_.size());
if (!index_status.ok()) {
return index_status;
}
const auto duration_status = validate_frame_duration(duration_ms);
if (!duration_status.ok()) {
return duration_status;
}
frames_[index].duration_ms = duration_ms;
for (auto& layer : layers_) {
if (index < layer.frames.size()) {
layer.frames[index].duration_ms = duration_ms;
}
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept
{
const auto index_status = validate_frame_index(index, frames_.size());
if (!index_status.ok()) {
return index_status;
}
active_frame_index_ = index;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels)
{
const auto layer_status = validate_layer_index(layer_index, layers_.size());
if (!layer_status.ok()) {
return layer_status;
}
const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
if (!frame_status.ok()) {
return frame_status;
}
const auto pixels_status = validate_face_pixels(pixels, width_, height_);
if (!pixels_status.ok()) {
return pixels_status;
}
auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
const auto existing = std::find_if(
faces.begin(),
faces.end(),
[face_index = pixels.face_index](const LayerFacePixels& face) {
return face.face_index == face_index;
});
if (existing == faces.end()) {
faces.push_back(std::move(pixels));
} else {
*existing = std::move(pixels);
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask)
{
const auto mask_status = validate_selection_mask(mask, width_, height_);
if (!mask_status.ok()) {
return mask_status;
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index = mask.face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
selection_masks_.push_back(std::move(mask));
} else {
*existing = std::move(mask);
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept
{
if (face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
return pp::foundation::Status::out_of_range("selection mask face is not present");
}
selection_masks_.erase(existing);
return pp::foundation::Status::success();
}
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
CanvasDocument initial_document,
std::size_t max_entries)
{
if (max_entries < min_document_history_entries) {
return pp::foundation::Result<DocumentHistory>::failure(
pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
}
if (max_entries > max_document_history_entries) {
return pp::foundation::Result<DocumentHistory>::failure(
pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
}
DocumentHistory history;
history.max_entries_ = max_entries;
history.entries_.reserve(max_entries);
history.entries_.push_back(initial_document);
return pp::foundation::Result<DocumentHistory>::success(history);
}
const CanvasDocument& DocumentHistory::current() const noexcept
{
return entries_[current_index_];
}
std::size_t DocumentHistory::size() const noexcept
{
return entries_.size();
}
std::size_t DocumentHistory::current_index() const noexcept
{
return current_index_;
}
bool DocumentHistory::can_undo() const noexcept
{
return current_index_ > 0;
}
bool DocumentHistory::can_redo() const noexcept
{
return current_index_ + 1U < entries_.size();
}
pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
{
if (entries_.empty()) {
return pp::foundation::Status::invalid_argument("document history is not initialized");
}
if (can_redo()) {
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
}
entries_.push_back(next_document);
if (entries_.size() > max_entries_) {
entries_.erase(entries_.begin());
} else {
++current_index_;
}
current_index_ = entries_.size() - 1U;
return pp::foundation::Status::success();
}
pp::foundation::Status DocumentHistory::undo() noexcept
{
if (!can_undo()) {
return pp::foundation::Status::out_of_range("document history has no undo entry");
}
--current_index_;
return pp::foundation::Status::success();
}
pp::foundation::Status DocumentHistory::redo() noexcept
{
if (!can_redo()) {
return pp::foundation::Status::out_of_range("document history has no redo entry");
}
++current_index_;
return pp::foundation::Status::success();
}
}

153
src/document/document.h Normal file
View File

@@ -0,0 +1,153 @@
#pragma once
#include "foundation/result.h"
#include "paint/blend.h"
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace pp::document {
constexpr std::uint32_t max_canvas_dimension = 131072;
constexpr std::uint32_t max_layer_count = 1024;
constexpr std::uint32_t max_frame_count = 100000;
constexpr std::uint32_t min_frame_duration_ms = 1;
constexpr std::size_t min_document_history_entries = 2;
constexpr std::size_t max_document_history_entries = 10000;
constexpr std::size_t max_layer_name_length = 128;
constexpr std::uint32_t cube_face_count = 6;
constexpr std::uint32_t rgba8_components = 4;
constexpr std::uint32_t alpha8_components = 1;
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
struct DocumentConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t layer_count = 1;
};
struct LayerFacePixels {
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::vector<std::uint8_t> rgba8;
};
struct SelectionMask {
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::vector<std::uint8_t> alpha8;
};
struct AnimationFrame {
std::uint32_t duration_ms = 100;
std::vector<LayerFacePixels> face_pixels;
};
struct Layer {
std::string name;
bool visible = true;
bool alpha_locked = false;
float opacity = 1.0F;
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
std::vector<AnimationFrame> frames;
};
struct DocumentLayerConfig {
std::string_view name;
bool visible = true;
bool alpha_locked = false;
float opacity = 1.0F;
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
std::span<const AnimationFrame> frames;
};
struct DocumentSnapshotConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::span<const DocumentLayerConfig> layers;
std::span<const AnimationFrame> frames;
std::span<const SelectionMask> selection_masks;
};
class CanvasDocument {
public:
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig config);
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create_from_snapshot(DocumentSnapshotConfig config);
[[nodiscard]] std::uint32_t width() const noexcept;
[[nodiscard]] std::uint32_t height() const noexcept;
[[nodiscard]] std::size_t active_layer_index() const noexcept;
[[nodiscard]] std::size_t active_frame_index() const noexcept;
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
[[nodiscard]] std::size_t selection_mask_payload_count() const noexcept;
[[nodiscard]] std::span<const Layer> layers() const noexcept;
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
[[nodiscard]] std::span<const SelectionMask> selection_masks() const noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> add_layer(std::string_view name);
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
[[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to);
[[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept;
[[nodiscard]] pp::foundation::Status rename_layer(std::size_t index, std::string_view name);
[[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> add_frame(std::uint32_t duration_ms);
[[nodiscard]] pp::foundation::Result<std::size_t> duplicate_frame(std::size_t index);
[[nodiscard]] pp::foundation::Status remove_frame(std::size_t index);
[[nodiscard]] pp::foundation::Status move_frame(std::size_t from, std::size_t to);
[[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept;
[[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_frame_face_pixels(
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels);
[[nodiscard]] pp::foundation::Status set_selection_mask(SelectionMask mask);
[[nodiscard]] pp::foundation::Status clear_selection_mask(std::uint32_t face_index) noexcept;
private:
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::vector<Layer> layers_;
std::vector<AnimationFrame> frames_;
std::vector<SelectionMask> selection_masks_;
};
class DocumentHistory {
public:
[[nodiscard]] static pp::foundation::Result<DocumentHistory> create(
CanvasDocument initial_document,
std::size_t max_entries = 256);
[[nodiscard]] const CanvasDocument& current() const noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] std::size_t current_index() const noexcept;
[[nodiscard]] bool can_undo() const noexcept;
[[nodiscard]] bool can_redo() const noexcept;
[[nodiscard]] pp::foundation::Status apply(CanvasDocument next_document);
[[nodiscard]] pp::foundation::Status undo() noexcept;
[[nodiscard]] pp::foundation::Status redo() noexcept;
private:
std::size_t max_entries_ = 0;
std::size_t current_index_ = 0;
std::vector<CanvasDocument> entries_;
};
}

107
src/document/ppi_export.cpp Normal file
View File

@@ -0,0 +1,107 @@
#include "document/ppi_export.h"
#include "assets/image_pixels.h"
#include "assets/ppi_header.h"
#include <cstdint>
#include <span>
#include <utility>
namespace pp::document {
namespace {
[[nodiscard]] pp::foundation::Result<std::uint32_t> ppi_blend_mode(
pp::paint::BlendMode blend_mode) noexcept
{
switch (blend_mode) {
case pp::paint::BlendMode::normal:
return pp::foundation::Result<std::uint32_t>::success(0);
case pp::paint::BlendMode::multiply:
return pp::foundation::Result<std::uint32_t>::success(1);
case pp::paint::BlendMode::screen:
return pp::foundation::Result<std::uint32_t>::success(2);
case pp::paint::BlendMode::color_dodge:
return pp::foundation::Result<std::uint32_t>::success(3);
case pp::paint::BlendMode::overlay:
return pp::foundation::Result<std::uint32_t>::success(4);
}
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::invalid_argument("document layer blend mode cannot be exported to PPI"));
}
}
pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
const CanvasDocument& document)
{
std::vector<std::vector<pp::assets::PpiFrameConfig>> frame_configs;
frame_configs.reserve(document.layers().size());
std::vector<pp::assets::PpiLayerConfig> layer_configs;
layer_configs.reserve(document.layers().size());
std::vector<std::vector<std::byte>> payloads;
payloads.reserve(document.face_pixel_payload_count());
std::vector<pp::assets::PpiDirtyFacePayloadConfig> dirty_faces;
dirty_faces.reserve(document.face_pixel_payload_count());
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
const auto& layer = document.layers()[layer_index];
const auto blend_mode = ppi_blend_mode(layer.blend_mode);
if (!blend_mode) {
return pp::foundation::Result<std::vector<std::byte>>::failure(blend_mode.status());
}
auto& frames = frame_configs.emplace_back();
frames.reserve(layer.frames.size());
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
const auto& frame = layer.frames[frame_index];
frames.push_back(pp::assets::PpiFrameConfig {
.duration_ms = frame.duration_ms,
});
for (const auto& face : frame.face_pixels) {
const auto encoded = pp::assets::encode_png_rgba8(
face.width,
face.height,
face.rgba8);
if (!encoded) {
return pp::foundation::Result<std::vector<std::byte>>::failure(encoded.status());
}
payloads.push_back(encoded.value());
const auto& payload = payloads.back();
dirty_faces.push_back(pp::assets::PpiDirtyFacePayloadConfig {
.layer_index = static_cast<std::uint32_t>(layer_index),
.frame_index = static_cast<std::uint32_t>(frame_index),
.face_index = face.face_index,
.x = face.x,
.y = face.y,
.width = face.width,
.height = face.height,
.png_rgba8 = std::span<const std::byte>(payload.data(), payload.size()),
});
}
}
layer_configs.push_back(pp::assets::PpiLayerConfig {
.name = layer.name,
.metadata = pp::assets::PpiLayerMetadataConfig {
.opacity = layer.opacity,
.blend_mode = blend_mode.value(),
.alpha_locked = layer.alpha_locked,
.visible = layer.visible,
},
.frames = std::span<const pp::assets::PpiFrameConfig>(frames.data(), frames.size()),
});
}
return pp::assets::create_ppi_project(pp::assets::PpiProjectConfig {
.width = document.width(),
.height = document.height(),
.layers = std::span<const pp::assets::PpiLayerConfig>(layer_configs.data(), layer_configs.size()),
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces.data(), dirty_faces.size()),
});
}
}

14
src/document/ppi_export.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "document/document.h"
#include "foundation/result.h"
#include <cstddef>
#include <vector>
namespace pp::document {
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
const CanvasDocument& document);
}

117
src/document/ppi_import.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "document/ppi_import.h"
#include <utility>
#include <span>
#include <vector>
namespace pp::document {
namespace {
[[nodiscard]] pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(
std::uint32_t blend_mode) noexcept
{
switch (blend_mode) {
case 0:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
case 1:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
case 2:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
case 3:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
case 4:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
default:
return pp::foundation::Result<pp::paint::BlendMode>::failure(
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
}
}
[[nodiscard]] pp::foundation::Result<CanvasDocument> document_from_ppi_index(
const pp::assets::PpiProjectIndex& project)
{
if (project.body.layers.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("PPI project has no layers"));
}
const auto& reference_frames = project.body.layers.front().frames;
if (reference_frames.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("PPI project has no frames"));
}
std::vector<AnimationFrame> frames;
frames.reserve(reference_frames.size());
for (const auto& frame : reference_frames) {
frames.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
}
std::vector<std::vector<AnimationFrame>> layer_frames;
layer_frames.reserve(project.body.layers.size());
std::vector<DocumentLayerConfig> layers;
layers.reserve(project.body.layers.size());
for (const auto& layer : project.body.layers) {
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
if (!blend_mode) {
return pp::foundation::Result<CanvasDocument>::failure(blend_mode.status());
}
auto& frame_list = layer_frames.emplace_back();
frame_list.reserve(layer.frames.size());
for (const auto& frame : layer.frames) {
frame_list.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
}
layers.push_back(DocumentLayerConfig {
.name = layer.name,
.visible = layer.visible,
.alpha_locked = layer.alpha_locked,
.opacity = layer.opacity,
.blend_mode = blend_mode.value(),
.frames = std::span<const AnimationFrame>(frame_list.data(), frame_list.size()),
});
}
return CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = project.body.summary.width,
.height = project.body.summary.height,
.layers = layers,
.frames = frames,
.selection_masks = {},
});
}
}
pp::foundation::Result<CanvasDocument> import_ppi_project_document(
const pp::assets::PpiDecodedProjectImages& project)
{
auto document = document_from_ppi_index(project.project);
if (!document) {
return document;
}
auto value = document.value();
for (const auto& face : project.faces) {
const auto status = value.set_layer_frame_face_pixels(
face.layer_index,
face.frame_index,
LayerFacePixels {
.face_index = face.face_index,
.x = face.descriptor.x0,
.y = face.descriptor.y0,
.width = face.image.width,
.height = face.image.height,
.rgba8 = face.image.pixels,
});
if (!status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(status);
}
}
return pp::foundation::Result<CanvasDocument>::success(std::move(value));
}
}

11
src/document/ppi_import.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include "assets/ppi_header.h"
#include "document/document.h"
namespace pp::document {
[[nodiscard]] pp::foundation::Result<CanvasDocument> import_ppi_project_document(
const pp::assets::PpiDecodedProjectImages& project);
}

View File

@@ -5,6 +5,152 @@
#include "asset.h"
#include "util.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace {
[[nodiscard]] GLint font_atlas_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format);
}
[[nodiscard]] GLint font_atlas_pixel_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format);
}
[[nodiscard]] GLenum texture_unit(std::uint32_t unit_index) noexcept
{
return static_cast<GLenum>(pp::renderer::gl::active_texture_unit(unit_index));
}
void gen_buffers_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenBuffers(static_cast<GLsizei>(count), ids);
}
void bind_buffer_adapter(std::uint32_t target, std::uint32_t buffer) noexcept
{
glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(buffer));
}
void buffer_data_adapter(
std::uint32_t target,
std::intptr_t byte_count,
const void* data,
std::uint32_t usage) noexcept
{
glBufferData(static_cast<GLenum>(target), static_cast<GLsizeiptr>(byte_count), data, static_cast<GLenum>(usage));
}
void gen_vertex_arrays_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenVertexArrays(static_cast<GLsizei>(count), ids);
}
void bind_vertex_array_adapter(std::uint32_t vertex_array) noexcept
{
glBindVertexArray(static_cast<GLuint>(vertex_array));
}
void enable_vertex_attrib_array_adapter(std::uint32_t index) noexcept
{
glEnableVertexAttribArray(static_cast<GLuint>(index));
}
void vertex_attrib_pointer_adapter(
std::uint32_t index,
std::int32_t component_count,
std::uint32_t component_type,
std::uint8_t normalized,
std::int32_t stride,
const void* offset) noexcept
{
glVertexAttribPointer(
static_cast<GLuint>(index),
static_cast<GLint>(component_count),
static_cast<GLenum>(component_type),
static_cast<GLboolean>(normalized),
static_cast<GLsizei>(stride),
offset);
}
void draw_elements_adapter(
std::uint32_t mode,
std::int32_t count,
std::uint32_t index_type,
const void* index_offset) noexcept
{
glDrawElements(
static_cast<GLenum>(mode),
static_cast<GLsizei>(count),
static_cast<GLenum>(index_type),
index_offset);
}
void draw_arrays_adapter(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept
{
glDrawArrays(static_cast<GLenum>(mode), static_cast<GLint>(first), static_cast<GLsizei>(count));
}
[[nodiscard]] std::span<const pp::renderer::gl::OpenGlVertexAttribute> text_mesh_vertex_attributes() noexcept
{
static const std::array<pp::renderer::gl::OpenGlVertexAttribute, 2> attributes {
pp::renderer::gl::OpenGlVertexAttribute {
.index = 0U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = 0U,
},
pp::renderer::gl::OpenGlVertexAttribute {
.index = 1U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = static_cast<std::uintptr_t>(sizeof(float) * 2),
},
};
return attributes;
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshCreateDispatch text_mesh_create_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshCreateDispatch {
.gen_buffers = gen_buffers_adapter,
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
.gen_vertex_arrays = gen_vertex_arrays_adapter,
.bind_vertex_array = bind_vertex_array_adapter,
.enable_vertex_attrib_array = enable_vertex_attrib_array_adapter,
.vertex_attrib_pointer = vertex_attrib_pointer_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlBufferUploadDispatch text_buffer_upload_dispatch() noexcept
{
return pp::renderer::gl::OpenGlBufferUploadDispatch {
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshDrawDispatch text_mesh_draw_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshDrawDispatch {
.bind_vertex_array = bind_vertex_array_adapter,
.draw_elements = draw_elements_adapter,
.draw_arrays = draw_arrays_adapter,
};
}
}
std::map<std::string, Font> FontManager::m_fonts;
Sampler FontManager::m_sampler;
@@ -52,7 +198,7 @@ bool Font::load(const std::string& ttf, int font_size, float font_scale)
// offset = 0;
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
calc_bounds();
font_tex.create(w, h, GL_R8, GL_RED, bitmap.get());
font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get());
file.close();
size = font_size;
return true;
@@ -155,18 +301,22 @@ bool TextMesh::create()
{
App::I->render_task([this]
{
glGenBuffers(2, font_buffers);
#if USE_VBO
glGenVertexArrays(1, &font_array);
glBindVertexArray(font_array);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
glBindVertexArray(0);
#endif // USE_VBO
const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(
pp::renderer::gl::OpenGlMeshUpload {
.vertex_data = nullptr,
.vertex_byte_count = 0,
.index_data = nullptr,
.index_byte_count = 0,
.indexed = true,
.vertex_array_count = 1U,
.attributes = text_mesh_vertex_attributes(),
},
text_mesh_create_dispatch());
if (mesh.ok()) {
font_buffers[0] = static_cast<GLuint>(mesh.value().vertex_buffer);
font_buffers[1] = static_cast<GLuint>(mesh.value().index_buffer);
font_array = static_cast<GLuint>(mesh.value().vertex_arrays[0]);
}
});
return true;
}
@@ -254,12 +404,24 @@ void TextMesh::update(const std::string& text, const std::string& font, int size
font_array_count = (int)idx.size();
App::I->render_task([&]
{
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(GLushort), idx.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(glm::vec4), v.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::element_array_buffer_target(),
.buffer_id = font_buffers[1],
.data = idx.data(),
.byte_count = static_cast<std::intptr_t>(idx.size() * sizeof(GLushort)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::array_buffer_target(),
.buffer_id = font_buffers[0],
.data = v.data(),
.byte_count = static_cast<std::intptr_t>(v.size() * sizeof(glm::vec4)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
});
}
}
@@ -269,27 +431,20 @@ void TextMesh::draw()
auto& f = FontManager::get(font, size, weight, italic);
if (f.font_tex.ready())
{
glActiveTexture(GL_TEXTURE0);
glActiveTexture(texture_unit(0U));
f.font_tex.bind();
FontManager::m_sampler.bind(0);
#if USE_VBO
glBindVertexArray(font_array);
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
glBindVertexArray(0);
#else
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
#endif // USE_VBO
(void)pp::renderer::gl::draw_opengl_mesh(
pp::renderer::gl::OpenGlMeshDraw {
.vertex_array = font_array,
.mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
.count = font_array_count,
.indexed = true,
.index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
.index_offset = nullptr,
},
text_mesh_draw_dispatch());
f.font_tex.unbind();
FontManager::m_sampler.unbind();

View File

@@ -0,0 +1,169 @@
#include "foundation/binary_stream.h"
#include <cstdint>
namespace pp::foundation {
namespace {
[[nodiscard]] bool overlaps_backing_storage(
const std::vector<std::byte>& backing,
std::span<const std::byte> bytes) noexcept
{
if (backing.empty() || bytes.empty()) {
return false;
}
const auto backing_begin = reinterpret_cast<std::uintptr_t>(backing.data());
const auto backing_end = backing_begin + backing.size();
const auto bytes_begin = reinterpret_cast<std::uintptr_t>(bytes.data());
const auto bytes_end = bytes_begin + bytes.size();
return bytes_begin < backing_end && backing_begin < bytes_end;
}
}
ByteReader::ByteReader(std::span<const std::byte> bytes) noexcept
: bytes_(bytes)
{
}
std::size_t ByteReader::position() const noexcept
{
return position_;
}
std::size_t ByteReader::size() const noexcept
{
return bytes_.size();
}
std::size_t ByteReader::remaining() const noexcept
{
return bytes_.size() - position_;
}
bool ByteReader::empty() const noexcept
{
return remaining() == 0;
}
Status ByteReader::seek(std::size_t position) noexcept
{
if (position > bytes_.size()) {
return Status::out_of_range("seek position is outside the stream");
}
position_ = position;
return Status::success();
}
Result<std::uint8_t> ByteReader::read_u8() noexcept
{
const auto bytes = read_bytes(1);
if (!bytes) {
return Result<std::uint8_t>::failure(bytes.status());
}
return Result<std::uint8_t>::success(static_cast<std::uint8_t>(bytes.value()[0]));
}
Result<std::uint16_t> ByteReader::read_u16_le() noexcept
{
const auto bytes = read_bytes(2);
if (!bytes) {
return Result<std::uint16_t>::failure(bytes.status());
}
const auto b0 = static_cast<std::uint16_t>(bytes.value()[0]);
const auto b1 = static_cast<std::uint16_t>(bytes.value()[1]);
return Result<std::uint16_t>::success(static_cast<std::uint16_t>(b0 | (b1 << 8U)));
}
Result<std::uint32_t> ByteReader::read_u32_le() noexcept
{
const auto bytes = read_bytes(4);
if (!bytes) {
return Result<std::uint32_t>::failure(bytes.status());
}
const auto b0 = static_cast<std::uint32_t>(bytes.value()[0]);
const auto b1 = static_cast<std::uint32_t>(bytes.value()[1]);
const auto b2 = static_cast<std::uint32_t>(bytes.value()[2]);
const auto b3 = static_cast<std::uint32_t>(bytes.value()[3]);
return Result<std::uint32_t>::success(b0 | (b1 << 8U) | (b2 << 16U) | (b3 << 24U));
}
Result<std::span<const std::byte>> ByteReader::read_bytes(std::size_t count) noexcept
{
if (count > remaining()) {
return Result<std::span<const std::byte>>::failure(
Status::out_of_range("read would move beyond the end of the stream"));
}
const auto start = position_;
position_ += count;
return Result<std::span<const std::byte>>::success(bytes_.subspan(start, count));
}
ByteWriter::ByteWriter(std::vector<std::byte>& bytes) noexcept
: bytes_(&bytes)
{
}
std::size_t ByteWriter::size() const noexcept
{
return bytes_ == nullptr ? 0 : bytes_->size();
}
Status ByteWriter::write_u8(std::uint8_t value)
{
if (bytes_ == nullptr) {
return Status::invalid_argument("writer has no backing storage");
}
bytes_->push_back(static_cast<std::byte>(value));
return Status::success();
}
Status ByteWriter::write_u16_le(std::uint16_t value)
{
if (bytes_ == nullptr) {
return Status::invalid_argument("writer has no backing storage");
}
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
return Status::success();
}
Status ByteWriter::write_u32_le(std::uint32_t value)
{
if (bytes_ == nullptr) {
return Status::invalid_argument("writer has no backing storage");
}
bytes_->push_back(static_cast<std::byte>(value & 0xffU));
bytes_->push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
bytes_->push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
bytes_->push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
return Status::success();
}
Status ByteWriter::write_bytes(std::span<const std::byte> bytes)
{
if (bytes_ == nullptr) {
return Status::invalid_argument("writer has no backing storage");
}
if (overlaps_backing_storage(*bytes_, bytes)) {
const std::vector<std::byte> copy(bytes.begin(), bytes.end());
bytes_->insert(bytes_->end(), copy.begin(), copy.end());
return Status::success();
}
bytes_->insert(bytes_->end(), bytes.begin(), bytes.end());
return Status::success();
}
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace pp::foundation {
class ByteReader {
public:
explicit ByteReader(std::span<const std::byte> bytes) noexcept;
[[nodiscard]] std::size_t position() const noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] std::size_t remaining() const noexcept;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] Status seek(std::size_t position) noexcept;
[[nodiscard]] Result<std::uint8_t> read_u8() noexcept;
[[nodiscard]] Result<std::uint16_t> read_u16_le() noexcept;
[[nodiscard]] Result<std::uint32_t> read_u32_le() noexcept;
[[nodiscard]] Result<std::span<const std::byte>> read_bytes(std::size_t count) noexcept;
private:
std::span<const std::byte> bytes_;
std::size_t position_ = 0;
};
class ByteWriter {
public:
explicit ByteWriter(std::vector<std::byte>& bytes) noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] Status write_u8(std::uint8_t value);
[[nodiscard]] Status write_u16_le(std::uint16_t value);
[[nodiscard]] Status write_u32_le(std::uint32_t value);
[[nodiscard]] Status write_bytes(std::span<const std::byte> bytes);
private:
std::vector<std::byte>* bytes_ = nullptr;
};
}

97
src/foundation/event.cpp Normal file
View File

@@ -0,0 +1,97 @@
#include "foundation/event.h"
#include <algorithm>
namespace pp::foundation {
EventDispatcher::EventDispatcher(std::size_t max_subscriptions) noexcept
: max_subscriptions_(max_subscriptions)
{
subscriptions_.reserve(std::min(max_subscriptions_, max_event_subscriptions));
}
std::size_t EventDispatcher::size() const noexcept
{
return subscriptions_.size();
}
bool EventDispatcher::empty() const noexcept
{
return subscriptions_.empty();
}
std::size_t EventDispatcher::max_subscriptions() const noexcept
{
return max_subscriptions_;
}
Result<std::uint64_t> EventDispatcher::subscribe(std::uint32_t type, EventCallback callback, void* user_data)
{
if (max_subscriptions_ == 0U || max_subscriptions_ > max_event_subscriptions) {
return Result<std::uint64_t>::failure(
Status::out_of_range("event dispatcher capacity is outside the configured range"));
}
if (type == 0U) {
return Result<std::uint64_t>::failure(Status::invalid_argument("event type must not be zero"));
}
if (callback == nullptr) {
return Result<std::uint64_t>::failure(Status::invalid_argument("event callback must not be null"));
}
if (subscriptions_.size() >= max_subscriptions_) {
return Result<std::uint64_t>::failure(Status::out_of_range("event dispatcher is full"));
}
const auto id = next_subscription_id_++;
subscriptions_.push_back(EventSubscription {
.id = id,
.type = type,
.callback = callback,
.user_data = user_data,
});
return Result<std::uint64_t>::success(id);
}
Status EventDispatcher::unsubscribe(std::uint64_t subscription_id) noexcept
{
const auto found = std::find_if(
subscriptions_.begin(),
subscriptions_.end(),
[subscription_id](const EventSubscription& subscription) {
return subscription.id == subscription_id;
});
if (found == subscriptions_.end()) {
return Status::out_of_range("event subscription id was not found");
}
subscriptions_.erase(found);
return Status::success();
}
std::size_t EventDispatcher::publish(const Event& event) const noexcept
{
if (event.type == 0U) {
return 0;
}
std::size_t delivered = 0;
for (const auto& subscription : subscriptions_) {
if (subscription.type == event.type) {
subscription.callback(event, subscription.user_data);
++delivered;
}
}
return delivered;
}
void EventDispatcher::clear() noexcept
{
subscriptions_.clear();
}
}

48
src/foundation/event.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace pp::foundation {
constexpr std::size_t max_event_subscriptions = 65536;
struct Event {
std::uint32_t type = 0;
std::uint64_t source_id = 0;
std::uint64_t frame_id = 0;
std::uint64_t payload_u64 = 0;
};
using EventCallback = void (*)(const Event& event, void* user_data) noexcept;
struct EventSubscription {
std::uint64_t id = 0;
std::uint32_t type = 0;
EventCallback callback = nullptr;
void* user_data = nullptr;
};
class EventDispatcher {
public:
explicit EventDispatcher(std::size_t max_subscriptions = max_event_subscriptions) noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] std::size_t max_subscriptions() const noexcept;
[[nodiscard]] Result<std::uint64_t> subscribe(std::uint32_t type, EventCallback callback, void* user_data);
[[nodiscard]] Status unsubscribe(std::uint64_t subscription_id) noexcept;
[[nodiscard]] std::size_t publish(const Event& event) const noexcept;
void clear() noexcept;
private:
std::size_t max_subscriptions_ = max_event_subscriptions;
std::uint64_t next_subscription_id_ = 1;
std::vector<EventSubscription> subscriptions_;
};
}

93
src/foundation/log.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "foundation/log.h"
namespace pp::foundation {
namespace {
[[nodiscard]] bool should_write(LogLevel level, LogLevel min_level) noexcept
{
return static_cast<std::uint8_t>(level) >= static_cast<std::uint8_t>(min_level);
}
}
Logger::Logger(ILogSink& sink) noexcept
: sink_(&sink)
{
}
void Logger::set_min_level(LogLevel level) noexcept
{
min_level_ = level;
}
LogLevel Logger::min_level() const noexcept
{
return min_level_;
}
Status Logger::write(
LogLevel level,
std::string_view component,
std::string_view message,
std::uint64_t frame_id,
std::uint64_t stroke_id,
std::uint64_t thread_id) noexcept
{
if (component.empty()) {
return Status::invalid_argument("log component must not be empty");
}
if (message.empty()) {
return Status::invalid_argument("log message must not be empty");
}
if (!should_write(level, min_level_)) {
return Status::success();
}
sink_->write(LogRecord {
.level = level,
.component = std::string(component),
.message = std::string(message),
.frame_id = frame_id,
.stroke_id = stroke_id,
.thread_id = thread_id,
});
return Status::success();
}
void MemoryLogSink::write(const LogRecord& record) noexcept
{
records_.push_back(record);
}
const std::vector<LogRecord>& MemoryLogSink::records() const noexcept
{
return records_;
}
void MemoryLogSink::clear() noexcept
{
records_.clear();
}
const char* log_level_name(LogLevel level) noexcept
{
switch (level) {
case LogLevel::trace:
return "trace";
case LogLevel::debug:
return "debug";
case LogLevel::info:
return "info";
case LogLevel::warning:
return "warning";
case LogLevel::error:
return "error";
}
return "unknown";
}
}

67
src/foundation/log.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace pp::foundation {
enum class LogLevel : std::uint8_t {
trace,
debug,
info,
warning,
error,
};
struct LogRecord {
LogLevel level = LogLevel::info;
std::string component;
std::string message;
std::uint64_t frame_id = 0;
std::uint64_t stroke_id = 0;
std::uint64_t thread_id = 0;
};
class ILogSink {
public:
virtual ~ILogSink() = default;
virtual void write(const LogRecord& record) noexcept = 0;
};
class Logger {
public:
explicit Logger(ILogSink& sink) noexcept;
void set_min_level(LogLevel level) noexcept;
[[nodiscard]] LogLevel min_level() const noexcept;
[[nodiscard]] Status write(
LogLevel level,
std::string_view component,
std::string_view message,
std::uint64_t frame_id = 0,
std::uint64_t stroke_id = 0,
std::uint64_t thread_id = 0) noexcept;
private:
ILogSink* sink_ = nullptr;
LogLevel min_level_ = LogLevel::trace;
};
class MemoryLogSink final : public ILogSink {
public:
void write(const LogRecord& record) noexcept override;
[[nodiscard]] const std::vector<LogRecord>& records() const noexcept;
void clear() noexcept;
private:
std::vector<LogRecord> records_;
};
[[nodiscard]] const char* log_level_name(LogLevel level) noexcept;
}

37
src/foundation/parse.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "foundation/parse.h"
#include <charconv>
namespace pp::foundation {
Result<std::uint32_t> parse_u32(std::string_view text) noexcept
{
if (text.empty()) {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must not be empty"));
}
if (text.front() == '-' || text.front() == '+') {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must be an unsigned integer without a sign"));
}
std::uint32_t value = 0;
const auto* begin = text.data();
const auto* end = text.data() + text.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec == std::errc::result_out_of_range) {
return Result<std::uint32_t>::failure(
Status::out_of_range("value is outside the uint32 range"));
}
if (ec != std::errc {} || ptr != end) {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must contain only decimal digits"));
}
return Result<std::uint32_t>::success(value);
}
}

12
src/foundation/parse.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string_view>
namespace pp::foundation {
[[nodiscard]] Result<std::uint32_t> parse_u32(std::string_view text) noexcept;
}

87
src/foundation/result.h Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
#include <utility>
namespace pp::foundation {
enum class StatusCode {
ok,
invalid_argument,
out_of_range,
};
struct Status {
StatusCode code = StatusCode::ok;
const char* message = "ok";
[[nodiscard]] constexpr bool ok() const noexcept
{
return code == StatusCode::ok;
}
[[nodiscard]] static constexpr Status success() noexcept
{
return {};
}
[[nodiscard]] static constexpr Status invalid_argument(const char* message) noexcept
{
return { StatusCode::invalid_argument, message };
}
[[nodiscard]] static constexpr Status out_of_range(const char* message) noexcept
{
return { StatusCode::out_of_range, message };
}
};
template <typename T>
class Result {
public:
[[nodiscard]] static constexpr Result success(T value) noexcept
{
return Result(std::move(value), Status::success());
}
[[nodiscard]] static constexpr Result failure(Status status) noexcept
{
return Result(T{}, status);
}
[[nodiscard]] constexpr bool ok() const noexcept
{
return status_.ok();
}
[[nodiscard]] constexpr explicit operator bool() const noexcept
{
return ok();
}
[[nodiscard]] constexpr const T& value() const noexcept
{
return value_;
}
[[nodiscard]] constexpr T& value() noexcept
{
return value_;
}
[[nodiscard]] constexpr Status status() const noexcept
{
return status_;
}
private:
constexpr Result(T value, Status status) noexcept
: value_(std::move(value))
, status_(status)
{
}
T value_{};
Status status_{};
};
}

View File

@@ -0,0 +1,83 @@
#include "foundation/task_queue.h"
namespace pp::foundation {
TaskQueue::TaskQueue(std::size_t max_entries) noexcept
: max_entries_(max_entries)
{
}
std::size_t TaskQueue::size() const noexcept
{
return tasks_.size();
}
bool TaskQueue::empty() const noexcept
{
return tasks_.empty();
}
std::size_t TaskQueue::max_entries() const noexcept
{
return max_entries_;
}
Status TaskQueue::push(TaskItem task)
{
if (max_entries_ == 0U || max_entries_ > max_task_queue_entries) {
return Status::out_of_range("task queue capacity is outside the configured range");
}
if (task.callback == nullptr) {
return Status::invalid_argument("task callback must not be null");
}
if (tasks_.size() >= max_entries_) {
return Status::out_of_range("task queue is full");
}
tasks_.push_back(task);
return Status::success();
}
Result<TaskItem> TaskQueue::pop() noexcept
{
if (tasks_.empty()) {
return Result<TaskItem>::failure(Status::out_of_range("task queue is empty"));
}
const auto task = tasks_.front();
tasks_.pop_front();
return Result<TaskItem>::success(task);
}
Status TaskQueue::run_next() noexcept
{
auto task = pop();
if (!task.ok()) {
return task.status();
}
task.value().callback(task.value().user_data);
return Status::success();
}
std::size_t TaskQueue::run_all() noexcept
{
std::size_t count = 0;
while (!tasks_.empty()) {
const auto status = run_next();
if (!status.ok()) {
break;
}
++count;
}
return count;
}
void TaskQueue::clear() noexcept
{
tasks_.clear();
}
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <deque>
namespace pp::foundation {
constexpr std::size_t max_task_queue_entries = 65536;
using TaskCallback = void (*)(void* user_data) noexcept;
struct TaskItem {
TaskCallback callback = nullptr;
void* user_data = nullptr;
std::uint64_t id = 0;
};
class TaskQueue {
public:
explicit TaskQueue(std::size_t max_entries = max_task_queue_entries) noexcept;
[[nodiscard]] std::size_t size() const noexcept;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] std::size_t max_entries() const noexcept;
[[nodiscard]] Status push(TaskItem task);
[[nodiscard]] Result<TaskItem> pop() noexcept;
[[nodiscard]] Status run_next() noexcept;
[[nodiscard]] std::size_t run_all() noexcept;
void clear() noexcept;
private:
std::size_t max_entries_ = max_task_queue_entries;
std::deque<TaskItem> tasks_;
};
}

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