Compare commits
537 Commits
d0ef88be89
...
codex/mode
| Author | SHA1 | Date | |
|---|---|---|---|
| 458f9bef0c | |||
| 2ee6534918 | |||
| e5d5d5f9ce | |||
| 6c58b6bb5d | |||
| 33f21e0a1b | |||
| d69869f720 | |||
| 6cce9dd726 | |||
| 81726d30a5 | |||
| 57c6128d11 | |||
| e37b29296e | |||
| ec5f4b76ec | |||
| 648404eec6 | |||
| 876d19f481 | |||
| 397a6be6d9 | |||
| 46fb8efec4 | |||
| 90f5fb29a6 | |||
| 7dc4f773de | |||
| 85d3fd5b93 | |||
| 34a9e91099 | |||
| a4cc251c68 | |||
| 231dce8875 | |||
| ae24285203 | |||
| 8cd384012f | |||
| 4c0450c87f | |||
| 44f9e7fb68 | |||
| e489b1e28c | |||
| c98130a51f | |||
| c9c3df2733 | |||
| 598495dcc7 | |||
| c38e284be2 | |||
| a0dd313e0c | |||
| 570ccb2bfa | |||
| 4df92b9cd2 | |||
| 058997bd78 | |||
| 9f1a52401a | |||
| baee4b2a08 | |||
| d049d586ed | |||
| 59dd010b5a | |||
| 14a3721e0d | |||
| be4f5b0a31 | |||
| a25ec420fe | |||
| 2c42a1e4d8 | |||
| 48a795822a | |||
| a6b01c2d12 | |||
| b9b0663546 | |||
| 7457b06cf9 | |||
| e89d882022 | |||
| 84373f26e7 | |||
| bfaea5398e | |||
| 24cd14c172 | |||
| 3be0f7468c | |||
| b87927b456 | |||
| adb61795a6 | |||
| 9ac2c541dc | |||
| 22748d9967 | |||
| 32cea98661 | |||
| b32ad1b720 | |||
| bc3d348632 | |||
| 6470c6a6a8 | |||
| 08f6515468 | |||
| 94ce1aec92 | |||
| 935e6972a5 | |||
| 0c41101f5f | |||
| 14ccf67acd | |||
| d60f4d30e2 | |||
| 9b482d7f6b | |||
| a63246f716 | |||
| bbdc746426 | |||
| 4c7c48a22c | |||
| 76c0ed3c10 | |||
| 2e98efa13a | |||
| 7be588d763 | |||
| a44222813f | |||
| c8b55b36f7 | |||
| f3834827b1 | |||
| a03db82307 | |||
| ed9709ade8 | |||
| 9d9b93abb1 | |||
| 772dc7332b | |||
| 9d9c87c0cb | |||
| 09df47879d | |||
| 41279c8743 | |||
| 7575f51c45 | |||
| 6c772a1c84 | |||
| ab36af0a8f | |||
| 5ff2992c0e | |||
| 65c7716d62 | |||
| 59c9b05d6c | |||
| 3101e65dd3 | |||
| 4071919124 | |||
| d963daae70 | |||
| 7a9dd150e3 | |||
| bd416f8473 | |||
| 875a0127d9 | |||
| 3be7171010 | |||
| 3c36be4b43 | |||
| 77268a28fb | |||
| ebc84373e6 | |||
| 2d33f9d928 | |||
| af28da4e83 | |||
| 27e7c60413 | |||
| 6151fb7a3d | |||
| 693923b7bd | |||
| 81898a5dcc | |||
| 9cafc39788 | |||
| ba5c3069e1 | |||
| 9a75782891 | |||
| f4f6eb903e | |||
| d0412e3bf9 | |||
| a9ef2c598c | |||
| d0e023556b | |||
| 7c6c5f3e36 | |||
| d4dad133ea | |||
| ee46a6497f | |||
| 26a2349c5f | |||
| 0e6c61e8a9 | |||
| bdd7a32ff5 | |||
| 308fb13075 | |||
| 0fb3bd09ac | |||
| 26470e0fe8 | |||
| 96d1903cf2 | |||
| ad9b91eeda | |||
| 96ff1c41e2 | |||
| d719a5a5e5 | |||
| 84e63c0d34 | |||
| 421f2713db | |||
| df21d673dd | |||
| 76a8db1ef8 | |||
| 65bf047d77 | |||
| 2641db35ac | |||
| 745a5898da | |||
| 03b999e60f | |||
| 92fa5b224a | |||
| 8f062fb0c4 | |||
| 321e5d6287 | |||
| 35477978e5 | |||
| 8a4ca331cb | |||
| e731c06330 | |||
| 3e5340b696 | |||
| 97fd7de955 | |||
| 1dc2ae4f21 | |||
| 711a9b5037 | |||
| c761cd39fd | |||
| ac4fef8346 | |||
| e17463bf5a | |||
| 0236fc6620 | |||
| c4d00258ff | |||
| b1d71f2621 | |||
| ab3637af9c | |||
| 48f98d337b | |||
| b534c4a4da | |||
| 407297dc2e | |||
| 903fe2d5a1 | |||
| 73564342fc | |||
| d9f294e8e6 | |||
| f225a81ec4 | |||
| fcc0e577b8 | |||
| 808a084ee3 | |||
| f46839bf5c | |||
| e5526c6d0a | |||
| 5def47cdcc | |||
| 062fdaa982 | |||
| a79ef4cda8 | |||
| a104f88360 | |||
| 942c053c19 | |||
| c50ea14a2a | |||
| 32c95b224f | |||
| b825d920d2 | |||
| 1df93c23f7 | |||
| 548b6d3ae5 | |||
| 48a4547f51 | |||
| f7979be80f | |||
| 678bf2dcd6 | |||
| e42afcc83f | |||
| 9373e07d3e | |||
| f42a6540be | |||
| e95861e9b7 | |||
| 31c26c3127 | |||
| d5403f082c | |||
| 75fd7faeb0 | |||
| bd6cdc20c5 | |||
| a9e12f2219 | |||
| 59210c28ea | |||
| 2feeffd6c8 | |||
| 841fbac8eb | |||
| db0ecb590c | |||
| 4ccedaae4c | |||
| c514ac99aa | |||
| 3cd1d46025 | |||
| 111cc8c892 | |||
| c9fb91ab48 | |||
| 2a4698e9f6 | |||
| 9190e9053a | |||
| b8c7cd6e99 | |||
| b65db6f617 | |||
| 7ade927beb | |||
| d0510e9fd2 | |||
| 5aa07b2953 | |||
| d55f26d637 | |||
| 24197c5f7e | |||
| abe3a86cc5 | |||
| 4c61a490ce | |||
| ce787ce186 | |||
| fc20851462 | |||
| 45802dfc7c | |||
| 6440bde002 | |||
| 15c58bfb21 | |||
| b9dbcd10d7 | |||
| f55b1882c0 | |||
| 967a15f15f | |||
| 51601adf6d | |||
| 1b771287f9 | |||
| 0bd1e92ee1 | |||
| f2cb0f2276 | |||
| 1057dd488a | |||
| 11c7d87330 | |||
| 0489c4229e | |||
| b2334e65c9 | |||
| e6831fcb28 | |||
| 63ea626cef | |||
| 08d8c1e82c | |||
| 7aadd1041a | |||
| 4bd29bee9f | |||
| e52fd3cbb5 | |||
| b1acd5118b | |||
| 52cf7628da | |||
| 148aceb705 | |||
| c698de1482 | |||
| 883be98557 | |||
| 401ce33498 | |||
| be4b88dec8 | |||
| 104358bc62 | |||
| cabfa44729 | |||
| dc369c89b0 | |||
| 7d992931d9 | |||
| 6419645e03 | |||
| 3c709f07e6 | |||
| ca452f46e1 | |||
| 576b58b061 | |||
| bc3973ef15 | |||
| 0c7bc98d5b | |||
| 47c35fb859 | |||
| 79942113ef | |||
| 394979e4fc | |||
| 6ab64ccc82 | |||
| 78185b8fd5 | |||
| 2bd1b12ade | |||
| 884a6d4940 | |||
| f8243566c4 | |||
| ca5b94b044 | |||
| 78003923ca | |||
| ab6223c256 | |||
| 8a0810acb3 | |||
| 4528edfb2c | |||
| d980b81bd7 | |||
| 1984b71a0a | |||
| a9ed201adf | |||
| 65e9fdf1b9 | |||
| bd2ee54617 | |||
| a2e795a356 | |||
| c3d85074ac | |||
| 22bbc93b43 | |||
| 7460453b80 | |||
| 855c388027 | |||
| d1bd4e9b46 | |||
| 6945ce7e23 | |||
| 16a1d1e15b | |||
| b184b3e075 | |||
| 10c995f1da | |||
| 921fc8f00b | |||
| 164f99fe48 | |||
| fa1493b843 | |||
| 6b92d0bfea | |||
| 2ac2c45b11 | |||
| b576143afb | |||
| bc5b39057d | |||
| 1369a9048e | |||
| a89f5e6cf2 | |||
| 2ec11e5099 | |||
| 94a6877e7c | |||
| dc23a5648d | |||
| 9adfad9609 | |||
| cee5f141a3 | |||
| 603bb0c4e7 | |||
| 5752bc6ae9 | |||
| 93f3037410 | |||
| 9c7c89fed4 | |||
| 45a7d49d40 | |||
| de9bca8bb5 | |||
| 6427f218e7 | |||
| 6d0cc4eb15 | |||
| a6306c2759 | |||
| 7c76703355 | |||
| 9c3f56954e | |||
| e880f23040 | |||
| defa9fc212 | |||
| ea96f38875 | |||
| b67f3d63cf | |||
| fb111dcdc9 | |||
| 62561624ed | |||
| b5bd6d42f7 | |||
| c640519772 | |||
| fb844f79fd | |||
| 6dac909869 | |||
| 65b262207c | |||
| ef50f4a361 | |||
| 888e94a77c | |||
| c56d301b29 | |||
| 91e1c2c9a3 | |||
| 2087505921 | |||
| 58afa672c7 | |||
| 8dc476d205 | |||
| 73fac0f8e4 | |||
| a487b0ba48 | |||
| efd568a416 | |||
| 4f0909f30c | |||
| fdc1defaba | |||
| 07ed23c2d1 | |||
| 5d5bb24711 | |||
| 21c448d6f1 | |||
| cd9206344d | |||
| 4d06608cc9 | |||
| a64a63def7 | |||
| 19cb14b5dc | |||
| 4de6f496ad | |||
| e1cce05bd6 | |||
| 1ae79ab3c1 | |||
| acdaf3bb8e | |||
| f20595aff6 | |||
| 779d6b0387 | |||
| 3128a0d309 | |||
| ae69f7437f | |||
| 9971b2b7f2 | |||
| 3e15b2f46c | |||
| 7dcf76c3aa | |||
| 155e67fcec | |||
| 2a030318b1 | |||
| 103fe4fb12 | |||
| b2335b1656 | |||
| 692fe08d9f | |||
| 8b12ae35d4 | |||
| 87b1851d59 | |||
| 389cd93e68 | |||
| 6652127545 | |||
| e152616d7f | |||
| ac4d065c78 | |||
| 578b1f6082 | |||
| beb7f717f1 | |||
| 7a9b14a86f | |||
| f3925f8423 | |||
| dd641c047b | |||
| 22006eaf47 | |||
| 537f0dcb2f | |||
| 2ea850cbcc | |||
| e10e16f491 | |||
| 6369c3c969 | |||
| ead7f58285 | |||
| 0e77ca6ba8 | |||
| 1e0500a3f7 | |||
| 4ed72ebc80 | |||
| 6960bd3410 | |||
| 5ee2dd271c | |||
| 5ac807c6bd | |||
| 4af55a7d3f | |||
| 712c28068d | |||
| 777723b68c | |||
| cc3490d9d8 | |||
| d9be3f910a | |||
| 8a7db3bca8 | |||
| 3a78361aea | |||
| 6e3296469a | |||
| 561193b2ab | |||
| 8de9dadf1d | |||
| 853307697a | |||
| fd1772a417 | |||
| 1df506a176 | |||
| b349f24931 | |||
| 5841878df9 | |||
| c8d769c02c | |||
| d28aa25358 | |||
| 76808d60e3 | |||
| 9dd53f9212 | |||
| 0e03e5940a | |||
| e15894e4ea | |||
| 37b1cf82f3 | |||
| 39444af84e | |||
| da584ce0f0 | |||
| 455c91bf29 | |||
| 6fda4d4a90 | |||
| b84dfc049d | |||
| 3a1ca7a8e6 | |||
| b80bd759aa | |||
| a2e47c862e | |||
| 7b882896f1 | |||
| def1a170dc | |||
| 6a3cd867f0 | |||
| 55b725e876 | |||
| d664e9fc39 | |||
| 1dcd96ab36 | |||
| b6a25474ff | |||
| b4c2117992 | |||
| 9a4c595f64 | |||
| ce33eaaef2 | |||
| cc33fbdde2 | |||
| c18297f221 | |||
| 2f8f12a8fd | |||
| 728116da8f | |||
| 36f9e73dd4 | |||
| 9b6c5b0849 | |||
| cc4eaef3e6 | |||
| 77c2a68cc5 | |||
| 647dd81992 | |||
| c5c31f0a56 | |||
| b6c66f3e41 | |||
| 1065183e75 | |||
| dc03491b0d | |||
| 8c99454bf5 | |||
| 0fc73d51d2 | |||
| 831e5deeae | |||
| 22dfde8e7c | |||
| 9a7f4bc0d2 | |||
| 860e5ad31e | |||
| 9b00acec6f | |||
| 53fc5f9a57 | |||
| f6780d183c | |||
| 48fdfd849d | |||
| 52da64fc96 | |||
| 9759abde44 | |||
| 06a44705d0 | |||
| 3ae84de123 | |||
| 8c0784f9c3 | |||
| 995752da75 | |||
| 18617cdbd2 | |||
| 56cb9eaacb | |||
| a5dbf05ab5 | |||
| bbe3db1747 | |||
| 07293c0590 | |||
| 901aff1051 | |||
| 75dd5cfdc9 | |||
| 483bbb4a9c | |||
| 58f163788b | |||
| 8232b0efc8 | |||
| 23c308db1b | |||
| 881b5271a2 | |||
| 952a00e7d3 | |||
| b68ddc42c6 | |||
| 9a7e1c4def | |||
| 5226746c1a | |||
| 5dbeb0504d | |||
| ee3fb36047 | |||
| 1c40602744 | |||
| 818014127a | |||
| d37145660a | |||
| c58b9a3718 | |||
| a6a4e7b249 | |||
| 2b50c2157f | |||
| 0eded78c4c | |||
| 99b2eeb99d | |||
| 7b14c356db | |||
| bad2670f87 | |||
| b3710498f3 | |||
| 1bc90d88b4 | |||
| ddca24779e | |||
| 1ab2a9b846 | |||
| 9c6b52eb8e | |||
| 9d05d193a7 | |||
| e6e80b94ba | |||
| 4e70c90ca8 | |||
| a8faa82b70 | |||
| 4f4ac380ac | |||
| 374cb5b075 | |||
| b0445382dd | |||
| 3701fd2a71 | |||
| 1e4b4cad73 | |||
| a6aa31da79 | |||
| b82cc1e4bd | |||
| 1d44036933 | |||
| 61f86f5aae | |||
| acd8ef6658 | |||
| c22f2e7fa2 | |||
| b7d9dfbf31 | |||
| 92338a0911 | |||
| f7d32f2835 | |||
| 737c29cca4 | |||
| 37a59c01ac | |||
| 7ae37038b3 | |||
| bbe8378630 | |||
| 466c1d0cc0 | |||
| 4a7eff24bf | |||
| f968488e34 | |||
| a12a3454c4 | |||
| 8a92bc973b | |||
| dbaf50cb6e | |||
| 92e9de0441 | |||
| 7280678593 | |||
| b7c087617b | |||
| d8e958769b | |||
| b85c530df7 | |||
| 3823a612ae | |||
| 2a3402e991 | |||
| bbb85bb133 | |||
| d0b0dc3865 | |||
| 6fc8b9e5d2 | |||
| 217450e161 | |||
| 3930f39b14 | |||
| 19f815e3d2 | |||
| 9e0a88726c | |||
| 47eb1ec0b2 | |||
| 0d2a1bd0ae | |||
| 85a5d19a3e | |||
| 02f14f1bf5 | |||
| e00eec30d4 | |||
| 43e3a74c42 | |||
| 75dfc85978 | |||
| 9ce49ef19c | |||
| 36fea6b870 | |||
| 8130a922d0 | |||
| f1e2743d58 | |||
| 7d80afce2f | |||
| 4212387b70 | |||
| bdcd44b340 | |||
| 05064b3a0d | |||
| aa32c47e18 | |||
| 2e0ebd0e13 | |||
| 2754df9f46 | |||
| 9ab73a0354 | |||
| d61c7f37c3 | |||
| ad255a6ddf | |||
| 88507df90e | |||
| 10e5d5b5ae | |||
| c16cab87bd | |||
| 7319cb9aa9 | |||
| 677d0b33a8 | |||
| f1ee1b28a1 | |||
| 2da247f0fb | |||
| 37854ea8b9 | |||
| dc252b2f24 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,6 +39,8 @@ PanoPainterPackage/_pkginfo.txt
|
||||
PanoPainterPackage/AppPackages/
|
||||
PanoPainterPackage/BundleArtifacts/
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
steam/content/
|
||||
steam/output/
|
||||
@@ -55,3 +57,4 @@ webgl/build
|
||||
webgl/.vscode
|
||||
|
||||
out/
|
||||
Testing/
|
||||
|
||||
172
AGENTS.md
Normal file
172
AGENTS.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file is the quick-start map for agents working in this repository. Keep it
|
||||
small and point to the live docs instead of duplicating the whole plan.
|
||||
|
||||
## Current Modernization Goal
|
||||
|
||||
PanoPainter is being incrementally modernized into independently testable C++23
|
||||
components while preserving current behavior. OpenGL remains the working backend
|
||||
until the renderer boundary is proven; Vulkan/Metal/WebGPU work waits behind that
|
||||
boundary.
|
||||
|
||||
Read these first:
|
||||
|
||||
- `docs/modernization/roadmap.md` - live phase roadmap, recent results, and next
|
||||
work queue.
|
||||
- `docs/modernization/debt.md` - required debt log. Every temporary adapter,
|
||||
retained legacy dependency, skipped platform, fallback, or shortcut needs an
|
||||
entry with validation and removal conditions.
|
||||
- `docs/modernization/capability-map.md` - behavior that must be preserved.
|
||||
- `docs/modernization/build-inventory.md` - current build/component inventory.
|
||||
- `docs/modernization/tasks.md` - measurable task tracker and scorecard.
|
||||
- `docs/modernization/director-workflow.md` - multi-agent director/captain
|
||||
workflow; use only when the user asks for subagents or delegation.
|
||||
- `docs/adr/0001-modernization-boundaries.md` - component boundary decisions.
|
||||
|
||||
## Working Rules
|
||||
|
||||
- Work on `codex/modernization-cmake-foundation` unless the user says otherwise.
|
||||
- Commit and push each verified successful progress slice.
|
||||
- Prefer larger coherent slices over tiny checkpoints, but keep docs/debt updated
|
||||
with each slice.
|
||||
- After a verified slice is committed and pushed, reset conversation context
|
||||
before starting the next slice when practical, especially if the thread is
|
||||
approaching automatic compaction. Record all needed resume state in committed
|
||||
code/docs first so the next thread can restart from `AGENTS.md`, roadmap/debt,
|
||||
and git history instead of relying on chat transcript context.
|
||||
- Do not revert user changes. Unrelated untracked notes, such as
|
||||
`docs/human-review-notes.md`, should be left alone unless explicitly requested.
|
||||
- Use CMake as the source of truth. Legacy Visual Studio project files are not the
|
||||
modernization path.
|
||||
- Use `apply_patch` for manual source/doc edits.
|
||||
- For delegated work, follow `docs/modernization/director-workflow.md`: the
|
||||
director keeps integration locally, assigns `gpt-5.4` captains to coherent
|
||||
task groups, and uses lighter workers only for bounded disjoint subtasks.
|
||||
|
||||
## Build And Test
|
||||
|
||||
Primary Windows configure/build:
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-msvc-default
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
Quiet checkpoint validation, preferred when working through Codex token-limited
|
||||
sessions:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"
|
||||
```
|
||||
|
||||
The quiet wrapper writes full command logs under `out/logs/quiet-validation`,
|
||||
prints only a compact summary, and applies editable warning/noise filters from
|
||||
`scripts/automation/quiet-validation-ignore.txt`. If a step fails, read the
|
||||
reported log file instead of rerunning with verbose output.
|
||||
|
||||
Focused fast validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug --output-on-failure
|
||||
```
|
||||
|
||||
Useful targeted pattern:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core|pano_cli_plan" --output-on-failure
|
||||
```
|
||||
|
||||
Apple compile validation runs through the Mac mini SSH alias `panopainter-mac`:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device
|
||||
```
|
||||
|
||||
This is a headless component/test/tool compile gate for macOS, iOS simulator,
|
||||
and iOS device. Signed Apple bundles remain tracked in the debt log.
|
||||
|
||||
If MSVC reports corrupt debug information or stale PDB/object errors, clean the
|
||||
generated build tree target before judging source changes:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-msvc-default --config Debug --target clean
|
||||
```
|
||||
|
||||
## Code Navigation
|
||||
|
||||
Codex has a repo-specific skill named `panopainter-code-navigation`. Agents must
|
||||
use it when necessary: follow this path before broad text search when a task is
|
||||
about C++ symbol identity, symbol families, declarations/definitions, override
|
||||
groups, or platform/backend boundaries. Use it when
|
||||
following C++ symbols, finding symbol families with regular expressions, tracing
|
||||
service/interface wiring, or checking platform/backend boundary usage.
|
||||
Reach for `--name-regex`,
|
||||
`--detail-regex`, or `--path-regex` when looking for generated-style name
|
||||
families, override groups, command/service families, signatures, or
|
||||
platform/backend path slices. Use normal `rg` for docs, build files, comments,
|
||||
string literals, generated command names, or exact non-C++ text.
|
||||
|
||||
Prefer compiler-aware navigation when following C++ symbols across the legacy
|
||||
flat source tree and extracted components:
|
||||
|
||||
```powershell
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
The helper talks to `clangd` using an existing `compile_commands.json`. It
|
||||
defaults to `out/build/windows-clangcl-asan` and then `out/build/android-arm64`;
|
||||
pass `--compile-commands-dir` or set `PP_CLANGD_COMPILE_COMMANDS_DIR` when using
|
||||
another Ninja build tree. Use `--name` and `--max-results` to keep output small.
|
||||
Use `--name-regex` for regex filtering against `qualifiedName`,
|
||||
`--detail-regex` for symbol detail/signature filtering, and `--path-regex` for
|
||||
definition/declaration/implementation/reference location filtering. Regex
|
||||
matching is case-insensitive by default, and `--no-ignore-case` makes it
|
||||
case-sensitive. Run `python scripts/dev/clangd_nav.py self-test` or the
|
||||
`panopainter_clangd_nav_regex_self_test` CTest before relying on regex behavior
|
||||
after tool changes.
|
||||
Treat symbol, hover, declaration, definition, and implementation lookups as the
|
||||
reliable path. Reference lookups are riskier because a one-shot clangd process
|
||||
may not have a complete project index; the helper refuses reference queries
|
||||
unless callers pass `--background-index` for broader best-effort results or
|
||||
`--allow-incomplete-references` for explicitly current-translation-unit-only
|
||||
results. Do not use incomplete reference output as proof that a symbol has no
|
||||
other users.
|
||||
|
||||
## Current Architecture Direction
|
||||
|
||||
The desired component split is documented in the roadmap. Current extracted or
|
||||
in-progress boundaries include:
|
||||
|
||||
- `pp_foundation`
|
||||
- `pp_assets`
|
||||
- `pp_paint`
|
||||
- `pp_document`
|
||||
- `pp_renderer_api`
|
||||
- `pp_renderer_gl`
|
||||
- `pp_paint_renderer`
|
||||
- `pp_app_core`
|
||||
- `pp_panopainter_ui`
|
||||
- `pp_platform_*`
|
||||
- `panopainter_app`
|
||||
|
||||
Legacy UI/app code still exists and is being reduced through pure planners,
|
||||
service interfaces, CLI automation, and CTest coverage. When moving behavior out
|
||||
of a legacy node, preserve current behavior first, add focused tests, document
|
||||
remaining shortcuts in `docs/modernization/debt.md`, then wire the live adapter.
|
||||
|
||||
## Legacy Context
|
||||
|
||||
PanoPainter started as a flat C++17 OpenGL panoramic painting app centered on
|
||||
singletons such as `App::I`, `Canvas::I`, `Settings`, and `WacomTablet::I`.
|
||||
Rendering uses GLAD/OpenGL and shaders in `data/shaders/`. UI is XML/Yoga based
|
||||
with many `Node*` subclasses. Project files are PPI, brush packages are PPBR,
|
||||
and Photoshop brushes import through ABR support.
|
||||
|
||||
Exceptions are disabled in app code. Public modernization APIs should return
|
||||
explicit status/result objects.
|
||||
462
CMakeLists.txt
462
CMakeLists.txt
@@ -20,6 +20,9 @@ endif()
|
||||
include(PanoPainterWarnings)
|
||||
include(PanoPainterSources)
|
||||
include(PanoPainterVersion)
|
||||
include(PanoPainterRuntime)
|
||||
include(PanoPainterPackageTargets)
|
||||
include(PanoPainterPlatformTargets)
|
||||
|
||||
if(PP_ENABLE_CLANG_TIDY)
|
||||
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
@@ -49,6 +52,13 @@ target_compile_features(pp_project_options INTERFACE cxx_std_23)
|
||||
add_library(pp_project_warnings INTERFACE)
|
||||
pp_configure_project_warnings(pp_project_warnings)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER
|
||||
"com.omigamedev.panopainter.$(PRODUCT_NAME:rfc1034identifier)")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
|
||||
endif()
|
||||
|
||||
if(PP_USE_VCPKG_TINYXML2)
|
||||
find_package(tinyxml2 CONFIG REQUIRED)
|
||||
add_library(pp_xml_tinyxml2 INTERFACE)
|
||||
@@ -96,13 +106,23 @@ target_link_libraries(pp_foundation
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_assets STATIC
|
||||
src/assets/brush_package.cpp
|
||||
src/assets/image_format.cpp
|
||||
src/assets/image_metadata.cpp
|
||||
src/assets/image_pixels.cpp
|
||||
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
|
||||
@@ -113,7 +133,8 @@ target_link_libraries(pp_assets
|
||||
add_library(pp_paint STATIC
|
||||
src/paint/brush.cpp
|
||||
src/paint/blend.cpp
|
||||
src/paint/stroke.cpp)
|
||||
src/paint/stroke.cpp
|
||||
src/paint/stroke_script.cpp)
|
||||
target_include_directories(pp_paint
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
@@ -125,20 +146,25 @@ target_link_libraries(pp_paint
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_document STATIC
|
||||
src/document/document.cpp)
|
||||
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/renderer_api.cpp)
|
||||
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")
|
||||
@@ -149,6 +175,33 @@ target_link_libraries(pp_renderer_api
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
if(PP_ENABLE_OPENGL)
|
||||
add_library(pp_renderer_gl STATIC
|
||||
src/renderer_gl/command_plan.cpp
|
||||
src/renderer_gl/opengl_capabilities.cpp
|
||||
src/renderer_gl/shader_bindings.cpp)
|
||||
target_include_directories(pp_renderer_gl
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_renderer_gl
|
||||
PUBLIC
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
if(EMSCRIPTEN)
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_GLES=1
|
||||
PP_RENDERER_GL_RUNTIME_WEB=1)
|
||||
elseif(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_GLES=1)
|
||||
else()
|
||||
target_compile_definitions(pp_renderer_gl PRIVATE
|
||||
PP_RENDERER_GL_RUNTIME_DESKTOP=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(pp_paint_renderer STATIC
|
||||
src/paint_renderer/compositor.cpp)
|
||||
target_include_directories(pp_paint_renderer
|
||||
@@ -157,6 +210,7 @@ target_include_directories(pp_paint_renderer
|
||||
target_link_libraries(pp_paint_renderer
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_renderer_api
|
||||
pp_project_options
|
||||
@@ -166,7 +220,9 @@ target_link_libraries(pp_paint_renderer
|
||||
add_library(pp_ui_core STATIC
|
||||
src/ui_core/color.cpp
|
||||
src/ui_core/layout_value.cpp
|
||||
src/ui_core/layout_xml.cpp)
|
||||
src/ui_core/layout_xml.cpp
|
||||
src/ui_core/node_lifetime.cpp
|
||||
src/ui_core/overlay_lifetime.cpp)
|
||||
target_include_directories(pp_ui_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
@@ -178,6 +234,72 @@ target_link_libraries(pp_ui_core
|
||||
pp_xml_tinyxml2
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_platform_api STATIC
|
||||
src/platform_api/asset_file_load_policy.cpp
|
||||
src/platform_api/asset_file_load_policy.h
|
||||
src/platform_api/network_tls_policy.cpp
|
||||
src/platform_api/network_tls_policy.h
|
||||
src/platform_apple/apple_platform_services.cpp
|
||||
src/platform_apple/apple_platform_services.h
|
||||
src/platform_api/platform_policy.cpp
|
||||
src/platform_api/platform_policy.h
|
||||
src/platform_api/platform_services.cpp
|
||||
src/platform_api/platform_services.h)
|
||||
target_include_directories(pp_platform_api
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_platform_api
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_app_core STATIC
|
||||
src/app_core/about_menu.h
|
||||
src/app_core/app_dialog.h
|
||||
src/app_core/app_frame.h
|
||||
src/app_core/app_input.h
|
||||
src/app_core/app_preferences.h
|
||||
src/app_core/app_shutdown.h
|
||||
src/app_core/app_status.h
|
||||
src/app_core/app_startup.h
|
||||
src/app_core/app_thread.h
|
||||
src/app_core/brush_package_import.h
|
||||
src/app_core/brush_package_export.h
|
||||
src/app_core/brush_ui.h
|
||||
src/app_core/canvas_hotkey.h
|
||||
src/app_core/canvas_tool_ui.h
|
||||
src/app_core/canvas_view.h
|
||||
src/app_core/command_convert.h
|
||||
src/app_core/document_animation.h
|
||||
src/app_core/document_canvas.h
|
||||
src/app_core/document_cloud.h
|
||||
src/app_core/document_export.cpp
|
||||
src/app_core/document_import.h
|
||||
src/app_core/document_layer.h
|
||||
src/app_core/document_platform_io.h
|
||||
src/app_core/document_recording.h
|
||||
src/app_core/document_resize.h
|
||||
src/app_core/document_route.cpp
|
||||
src/app_core/document_sharing.h
|
||||
src/app_core/document_session.cpp
|
||||
src/app_core/file_menu.h
|
||||
src/app_core/grid_ui.h
|
||||
src/app_core/history_ui.h
|
||||
src/app_core/main_toolbar.h
|
||||
src/app_core/quick_ui.h
|
||||
src/app_core/tools_menu.h)
|
||||
target_include_directories(pp_app_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_app_core
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_document
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
|
||||
if(PP_BUILD_TOOLS)
|
||||
add_subdirectory(tools/pano_cli)
|
||||
endif()
|
||||
@@ -189,15 +311,252 @@ endif()
|
||||
|
||||
if(PP_BUILD_APP)
|
||||
if(WIN32)
|
||||
add_library(pp_legacy_app STATIC
|
||||
${PP_LEGACY_APP_SOURCES}
|
||||
${PP_VENDOR_SOURCES})
|
||||
set(PP_LEGACY_FMT_SOURCES
|
||||
libs/fmt/src/format.cc
|
||||
libs/fmt/src/posix.cc)
|
||||
set(PP_LEGACY_VENDOR_SOURCES ${PP_VENDOR_SOURCES})
|
||||
list(REMOVE_ITEM PP_LEGACY_VENDOR_SOURCES ${PP_LEGACY_FMT_SOURCES})
|
||||
set(PP_LEGACY_VENDOR_DEFINITIONS
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
|
||||
target_link_libraries(pp_legacy_app
|
||||
add_library(pp_legacy_vendor OBJECT
|
||||
${PP_LEGACY_VENDOR_SOURCES})
|
||||
target_link_libraries(pp_legacy_vendor
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_vendor
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026")
|
||||
|
||||
add_library(pp_legacy_fmt OBJECT
|
||||
${PP_LEGACY_FMT_SOURCES})
|
||||
target_link_libraries(pp_legacy_fmt
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_fmt
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
if(MSVC_VERSION GREATER_EQUAL 1945)
|
||||
set(PP_FMT_VS2026_COMPAT_HEADER "${CMAKE_CURRENT_BINARY_DIR}/compat/fmt-vs2026-secure-scl.h")
|
||||
file(WRITE "${PP_FMT_VS2026_COMPAT_HEADER}"
|
||||
"#pragma once\n"
|
||||
"#include <yvals.h>\n"
|
||||
"#ifdef _SECURE_SCL\n"
|
||||
"#undef _SECURE_SCL\n"
|
||||
"#endif\n")
|
||||
target_compile_options(pp_legacy_fmt
|
||||
PUBLIC
|
||||
/FI"${PP_FMT_VS2026_COMPAT_HEADER}")
|
||||
endif()
|
||||
target_compile_definitions(pp_legacy_vendor
|
||||
PUBLIC
|
||||
${PP_LEGACY_VENDOR_DEFINITIONS})
|
||||
target_compile_definitions(pp_legacy_fmt
|
||||
PUBLIC
|
||||
${PP_LEGACY_VENDOR_DEFINITIONS})
|
||||
set_target_properties(pp_legacy_vendor PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
set_target_properties(pp_legacy_fmt PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
add_library(pp_legacy_renderer_gl OBJECT
|
||||
${PP_LEGACY_RENDERER_GL_SOURCES})
|
||||
target_link_libraries(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_renderer_gl PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
target_include_directories(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_renderer_gl
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_renderer_gl PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_renderer_gl PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_assets_io OBJECT
|
||||
${PP_LEGACY_ASSETS_IO_SOURCES})
|
||||
target_link_libraries(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_platform_api
|
||||
pp_project_warnings)
|
||||
target_include_directories(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_assets_io
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_assets_io PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_assets_io PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_paint_document OBJECT
|
||||
${PP_LEGACY_PAINT_DOCUMENT_SOURCES})
|
||||
target_link_libraries(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_paint_document PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
target_include_directories(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
target_compile_definitions(pp_legacy_paint_document
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_paint_document PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
target_precompile_headers(pp_legacy_paint_document PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_engine STATIC
|
||||
${PP_LEGACY_ENGINE_SOURCES}
|
||||
$<TARGET_OBJECTS:pp_legacy_assets_io>
|
||||
$<TARGET_OBJECTS:pp_legacy_paint_document>
|
||||
$<TARGET_OBJECTS:pp_legacy_renderer_gl>
|
||||
$<TARGET_OBJECTS:pp_legacy_fmt>
|
||||
$<TARGET_OBJECTS:pp_legacy_vendor>)
|
||||
|
||||
target_link_libraries(pp_legacy_engine
|
||||
PUBLIC
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_document
|
||||
pp_paint
|
||||
pp_paint_renderer
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_engine PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_engine
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_engine
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_engine PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_engine PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_ui_core OBJECT
|
||||
${PP_LEGACY_UI_CORE_SOURCES})
|
||||
|
||||
target_link_libraries(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_engine
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_paint_renderer
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_ui_core PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
${PP_LEGACY_INCLUDE_DIRS})
|
||||
|
||||
target_compile_definitions(pp_legacy_ui_core
|
||||
PUBLIC
|
||||
ENUM_BITFIELDS_NOT_SUPPORTED
|
||||
UNICODE
|
||||
_UNICODE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
_CONSOLE
|
||||
WITH_CURL=1)
|
||||
set_target_properties(pp_legacy_ui_core PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_ui_core PRIVATE src/pch.h)
|
||||
|
||||
add_library(pp_legacy_app STATIC
|
||||
${PP_LEGACY_APP_SOURCES}
|
||||
$<TARGET_OBJECTS:pp_legacy_ui_core>)
|
||||
|
||||
target_link_libraries(pp_legacy_app
|
||||
PUBLIC
|
||||
pp_legacy_engine
|
||||
pp_legacy_ui_core
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_platform_api
|
||||
pp_renderer_api
|
||||
pp_project_warnings)
|
||||
if(TARGET pp_renderer_gl)
|
||||
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
|
||||
endif()
|
||||
|
||||
target_include_directories(pp_legacy_app
|
||||
PUBLIC
|
||||
@@ -218,19 +577,42 @@ if(PP_BUILD_APP)
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
|
||||
set_source_files_properties(${PP_VENDOR_SOURCES}
|
||||
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
set_source_files_properties(src/version.cpp
|
||||
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
|
||||
|
||||
add_executable(PanoPainter WIN32
|
||||
${PP_WINDOWS_APP_SOURCES})
|
||||
|
||||
target_link_libraries(PanoPainter
|
||||
PRIVATE
|
||||
pp_project_options
|
||||
pp_project_warnings
|
||||
add_library(pp_panopainter_ui STATIC
|
||||
${PP_PANOPAINTER_UI_SOURCES})
|
||||
target_link_libraries(pp_panopainter_ui
|
||||
PUBLIC
|
||||
pp_legacy_app
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_assets
|
||||
pp_platform_api
|
||||
pp_project_warnings)
|
||||
target_precompile_headers(pp_panopainter_ui REUSE_FROM pp_legacy_app)
|
||||
set_target_properties(pp_panopainter_ui PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
add_library(panopainter_app STATIC
|
||||
${PP_PANOPAINTER_APP_SOURCES})
|
||||
target_include_directories(panopainter_app
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(panopainter_app
|
||||
PUBLIC
|
||||
pp_app_core
|
||||
pp_legacy_app
|
||||
pp_panopainter_ui
|
||||
pp_platform_api
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
pp_project_warnings)
|
||||
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||
|
||||
add_library(pp_platform_windows OBJECT
|
||||
${PP_WINDOWS_PLATFORM_SOURCES})
|
||||
target_link_libraries(pp_platform_windows
|
||||
PUBLIC
|
||||
panopainter_app
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
|
||||
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
|
||||
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
|
||||
@@ -245,34 +627,28 @@ if(PP_BUILD_APP)
|
||||
shell32
|
||||
shlwapi
|
||||
user32
|
||||
wbemuuid)
|
||||
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)
|
||||
|
||||
target_precompile_headers(PanoPainter REUSE_FROM pp_legacy_app)
|
||||
set_target_properties(PanoPainter PROPERTIES
|
||||
VS_GLOBAL_CharacterSet "Unicode")
|
||||
|
||||
pp_add_version_generation(PanoPainter "$<IF:$<CONFIG:Debug>,debug,release>")
|
||||
|
||||
add_custom_command(TARGET PanoPainter POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/data"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/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:PanoPainter>/"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/libyuv.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
|
||||
"$<TARGET_FILE_DIR:PanoPainter>/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:PanoPainter>/openh264-2.0.0-win64.dll"
|
||||
VERBATIM)
|
||||
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()
|
||||
|
||||
@@ -43,12 +43,14 @@
|
||||
"name": "windows-msvc-default",
|
||||
"inherits": "base",
|
||||
"displayName": "Windows MSVC default generator",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64"
|
||||
},
|
||||
{
|
||||
"name": "windows-msvc-vcpkg-headless",
|
||||
"inherits": "platform-headless-base",
|
||||
"displayName": "Windows MSVC vcpkg headless",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "x64",
|
||||
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"cacheVariables": {
|
||||
@@ -200,6 +202,22 @@
|
||||
{
|
||||
"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": [
|
||||
@@ -250,6 +268,30 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterAndroidNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
link_directories(
|
||||
../../libs/curl-android-ios/android/${ANDROID_ABI}
|
||||
@@ -23,11 +28,68 @@ add_library(yuv SHARED IMPORTED)
|
||||
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/../../libs/libyuv/lib/android/${ANDROID_ABI}/libyuv.so)
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
@@ -128,6 +190,9 @@ add_library(
|
||||
../../src/node_metadata.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
src/main/cpp
|
||||
../src/cpp
|
||||
|
||||
20
android/cmake/PanoPainterAndroidVendorPatches.cmake
Normal file
20
android/cmake/PanoPainterAndroidVendorPatches.cmake
Normal file
@@ -0,0 +1,20 @@
|
||||
set(PP_ANDROID_VENDOR_PATCH_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
|
||||
function(pp_apply_android_nanort_patch)
|
||||
set(nanort_header "${PP_ANDROID_VENDOR_PATCH_DIR}/../../libs/nanort/nanort.h")
|
||||
file(READ "${nanort_header}" nanort_contents)
|
||||
|
||||
set(nanort_before " const size_t vertex_stride_bytes_;")
|
||||
set(nanort_after " size_t vertex_stride_bytes_;")
|
||||
|
||||
if(nanort_contents MATCHES "${nanort_before}")
|
||||
string(REPLACE
|
||||
"${nanort_before}"
|
||||
"${nanort_after}"
|
||||
nanort_contents
|
||||
"${nanort_contents}")
|
||||
file(WRITE "${nanort_header}" "${nanort_contents}")
|
||||
elseif(NOT nanort_contents MATCHES "${nanort_after}")
|
||||
message(FATAL_ERROR "Unexpected nanort.h layout; Android nanort patch could not be applied")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterFocusNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
# build native_app_glue as a static lib
|
||||
add_library(
|
||||
@@ -17,23 +22,81 @@ set_target_properties(
|
||||
${CMAKE_SOURCE_DIR}/../../libs/wave_sdk/wvr_client/lib/${ANDROID_ABI}/libwvr_api.so
|
||||
)
|
||||
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
# Specifies a library name, specifies whether the library is STATIC or
|
||||
# SHARED, and provides relative paths to the source code. You can
|
||||
# define multiple libraries by adding multiple add.library() commands,
|
||||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
../../libs/yoga/yoga/Utils.cpp
|
||||
../../libs/yoga/yoga/YGConfig.cpp
|
||||
../../libs/yoga/yoga/YGEnums.cpp
|
||||
../../libs/yoga/yoga/YGLayout.cpp
|
||||
../../libs/yoga/yoga/YGMarker.cpp
|
||||
../../libs/yoga/yoga/YGNode.cpp
|
||||
../../libs/yoga/yoga/YGNodePrint.cpp
|
||||
../../libs/yoga/yoga/YGStyle.cpp
|
||||
@@ -121,6 +184,9 @@ add_library(
|
||||
../../src/settings.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
../../libs/wave_sdk/wvr_client/include
|
||||
src/main/cpp
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(PanoPainterQuestNative LANGUAGES C CXX)
|
||||
|
||||
include(../cmake/PanoPainterAndroidVendorPatches.cmake)
|
||||
pp_apply_android_nanort_patch()
|
||||
|
||||
# build native_app_glue as a static lib
|
||||
add_library(
|
||||
@@ -25,23 +30,81 @@ set_target_properties(
|
||||
)
|
||||
|
||||
|
||||
set(PP_MODERN_COMPONENT_SOURCES
|
||||
../../src/app_core/document_export.cpp
|
||||
../../src/app_core/document_route.cpp
|
||||
../../src/app_core/document_session.cpp
|
||||
../../src/assets/brush_package.cpp
|
||||
../../src/assets/image_format.cpp
|
||||
../../src/assets/image_metadata.cpp
|
||||
../../src/assets/image_pixels.cpp
|
||||
../../src/assets/ppi_header.cpp
|
||||
../../src/assets/settings_document.cpp
|
||||
../../src/document/document.cpp
|
||||
../../src/document/ppi_export.cpp
|
||||
../../src/document/ppi_import.cpp
|
||||
../../src/foundation/binary_stream.cpp
|
||||
../../src/foundation/event.cpp
|
||||
../../src/foundation/log.cpp
|
||||
../../src/foundation/parse.cpp
|
||||
../../src/foundation/task_queue.cpp
|
||||
../../src/foundation/trace.cpp
|
||||
../../src/paint/blend.cpp
|
||||
../../src/paint/brush.cpp
|
||||
../../src/paint/stroke.cpp
|
||||
../../src/paint/stroke_script.cpp
|
||||
../../src/paint_renderer/compositor.cpp
|
||||
../../src/platform_api/asset_file_load_policy.cpp
|
||||
../../src/platform_api/network_tls_policy.cpp
|
||||
../../src/platform_api/platform_policy.cpp
|
||||
../../src/platform_api/platform_services.cpp
|
||||
../../src/platform_legacy/legacy_platform_services.cpp
|
||||
../../src/renderer_api/recording_renderer.cpp
|
||||
../../src/renderer_api/renderer_api.cpp
|
||||
../../src/renderer_api/shader_catalog.cpp
|
||||
../../src/renderer_gl/command_plan.cpp
|
||||
../../src/renderer_gl/opengl_capabilities.cpp
|
||||
../../src/renderer_gl/shader_bindings.cpp
|
||||
../../src/legacy_app_dialog_services.cpp
|
||||
../../src/legacy_app_preference_services.cpp
|
||||
../../src/legacy_app_shell_services.cpp
|
||||
../../src/legacy_app_startup_services.cpp
|
||||
../../src/legacy_brush_package_export_services.cpp
|
||||
../../src/legacy_brush_package_import_services.cpp
|
||||
../../src/legacy_brush_ui_services.cpp
|
||||
../../src/legacy_canvas_tool_services.cpp
|
||||
../../src/legacy_canvas_view_services.cpp
|
||||
../../src/legacy_cloud_services.cpp
|
||||
../../src/legacy_document_animation_services.cpp
|
||||
../../src/legacy_document_canvas_services.cpp
|
||||
../../src/legacy_document_export_services.cpp
|
||||
../../src/legacy_document_layer_services.cpp
|
||||
../../src/legacy_document_open_services.cpp
|
||||
../../src/legacy_document_session_services.cpp
|
||||
../../src/legacy_grid_ui_services.cpp
|
||||
../../src/legacy_history_services.cpp
|
||||
../../src/legacy_preference_storage.cpp
|
||||
../../src/legacy_quick_ui_services.cpp
|
||||
../../src/legacy_recording_services.cpp
|
||||
../../src/legacy_ui_overlay_services.cpp
|
||||
)
|
||||
|
||||
# Specifies a library name, specifies whether the library is STATIC or
|
||||
# SHARED, and provides relative paths to the source code. You can
|
||||
# define multiple libraries by adding multiple add.library() commands,
|
||||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
|
||||
# now build app's shared lib
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
|
||||
add_library(
|
||||
native-lib SHARED
|
||||
${PP_MODERN_COMPONENT_SOURCES}
|
||||
../../libs/yoga/yoga/event/event.cpp
|
||||
../../libs/yoga/yoga/internal/experiments.cpp
|
||||
../../libs/yoga/yoga/log.cpp
|
||||
../../libs/yoga/yoga/Utils.cpp
|
||||
../../libs/yoga/yoga/YGConfig.cpp
|
||||
../../libs/yoga/yoga/YGEnums.cpp
|
||||
../../libs/yoga/yoga/YGLayout.cpp
|
||||
../../libs/yoga/yoga/YGMarker.cpp
|
||||
../../libs/yoga/yoga/YGNode.cpp
|
||||
../../libs/yoga/yoga/YGNodePrint.cpp
|
||||
../../libs/yoga/yoga/YGStyle.cpp
|
||||
@@ -129,6 +192,9 @@ add_library(
|
||||
../../src/settings.cpp
|
||||
)
|
||||
|
||||
target_compile_features(native-lib PRIVATE cxx_std_23)
|
||||
set_target_properties(native-lib PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
target_include_directories(native-lib PRIVATE
|
||||
../../libs/ovr_mobile/include
|
||||
../../libs/ovr_platform/Include
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
#include "keymap.h"
|
||||
#include "main.h"
|
||||
#include "settings.h"
|
||||
#if __has_include("com_omixlab_panopainter_MainActivity.h")
|
||||
#include "com_omixlab_panopainter_MainActivity.h"
|
||||
#endif
|
||||
|
||||
#ifdef __QUEST__
|
||||
#include "oculus_vr.h"
|
||||
@@ -241,7 +243,7 @@ extern "C"
|
||||
#ifdef __FOCUS__
|
||||
JNIEXPORT void JNICALL Java_com_omixlab_panopainter_MainActivity_init_vr(JNIEnv *env, jobject activity, jobject am)
|
||||
{
|
||||
Asset::m_am = AAssetManager_fromJava(env, am);
|
||||
Asset::set_android_asset_manager(AAssetManager_fromJava(env, am));
|
||||
wave_init(0, 0, 0);
|
||||
}
|
||||
#endif
|
||||
@@ -700,7 +702,7 @@ static int engine_init_display(struct engine* engine) {
|
||||
LOG("PROP Maker: %s", os_props["ro.product.manufacturer"].c_str());
|
||||
LOG("PROP Mode: %s", os_props["ro.product.model"].c_str());
|
||||
|
||||
Asset::m_am = engine->app->activity->assetManager;
|
||||
Asset::set_android_asset_manager(engine->app->activity->assetManager);
|
||||
App::I->and_app = engine->app;
|
||||
App::I->and_engine = engine;
|
||||
|
||||
|
||||
23
cmake/PanoPainterAutomation.cmake
Normal file
23
cmake/PanoPainterAutomation.cmake
Normal file
@@ -0,0 +1,23 @@
|
||||
find_program(PP_POWERSHELL_COMMAND NAMES pwsh powershell)
|
||||
|
||||
function(pp_add_powershell_automation_target target_name)
|
||||
cmake_parse_arguments(PP_AUTOMATION_TARGET "" "COMMENT" "ARGUMENTS" ${ARGN})
|
||||
|
||||
if(PP_POWERSHELL_COMMAND)
|
||||
add_custom_target(${target_name}
|
||||
COMMAND "${PP_POWERSHELL_COMMAND}"
|
||||
-NoProfile
|
||||
-ExecutionPolicy Bypass
|
||||
${PP_AUTOMATION_TARGET_ARGUMENTS}
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
COMMENT "${PP_AUTOMATION_TARGET_COMMENT}"
|
||||
USES_TERMINAL
|
||||
VERBATIM)
|
||||
else()
|
||||
add_custom_target(${target_name}
|
||||
COMMAND "${CMAKE_COMMAND}" -E echo "PowerShell was not found; cannot run ${target_name}."
|
||||
COMMAND "${CMAKE_COMMAND}" -E false
|
||||
COMMENT "${PP_AUTOMATION_TARGET_COMMENT}"
|
||||
VERBATIM)
|
||||
endif()
|
||||
endfunction()
|
||||
44
cmake/PanoPainterPackageTargets.cmake
Normal file
44
cmake/PanoPainterPackageTargets.cmake
Normal file
@@ -0,0 +1,44 @@
|
||||
include(PanoPainterAutomation)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_package_readiness
|
||||
COMMENT "Report package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_windows_app_package_smoke
|
||||
COMMENT "Build the Windows app artifact and report package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-Preset windows-msvc-default
|
||||
-Configuration Debug
|
||||
-Target PanoPainter
|
||||
-CMakeCommand "${CMAKE_COMMAND}")
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_standard_native_package
|
||||
COMMENT "Build retained Android standard native package library."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
|
||||
-Packages standard)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_vr_native_package_configure
|
||||
COMMENT "Configure retained Android Quest/Focus native package paths."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/android-legacy-package-build.ps1"
|
||||
-Packages quest,focus
|
||||
-ConfigureOnly)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_android_native_package_smoke
|
||||
COMMENT "Run retained Android native package checks through package-smoke."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-AndroidNativeChecks
|
||||
-PackageKinds android-standard-apk,android-quest-apk,android-focus-apk)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_linux_webgl_package_readiness
|
||||
COMMENT "Report retained Linux and WebGL package readiness blockers."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/package-smoke.ps1"
|
||||
-ReadinessOnly
|
||||
-PackageKinds linux-app,webgl)
|
||||
26
cmake/PanoPainterPlatformTargets.cmake
Normal file
26
cmake/PanoPainterPlatformTargets.cmake
Normal file
@@ -0,0 +1,26 @@
|
||||
include(PanoPainterAutomation)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_headless
|
||||
COMMENT "Run the default headless platform build matrix."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1")
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_android_assets
|
||||
COMMENT "Build Android root CMake asset component across standard/VR presets."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1"
|
||||
-Presets android-arm64,android-x64,android-quest-arm64,android-focus-arm64
|
||||
-Targets pp_assets)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_vcpkg_ui_core
|
||||
COMMENT "Build the Windows vcpkg-backed UI core dependency boundary."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/platform-build.ps1"
|
||||
-Presets windows-msvc-vcpkg-headless
|
||||
-Targets pp_ui_core,pp_ui_core_layout_xml_tests)
|
||||
|
||||
pp_add_powershell_automation_target(panopainter_platform_build_apple_remote
|
||||
COMMENT "Run remote Apple compile validation for macOS, iOS simulator, and iOS device."
|
||||
ARGUMENTS
|
||||
-File "${CMAKE_CURRENT_SOURCE_DIR}/scripts/automation/apple-remote-build.ps1"
|
||||
-Presets macos,ios-simulator,ios-device)
|
||||
29
cmake/PanoPainterRuntime.cmake
Normal file
29
cmake/PanoPainterRuntime.cmake
Normal file
@@ -0,0 +1,29 @@
|
||||
function(pp_configure_windows_runtime_payloads target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "pp_configure_windows_runtime_payloads target does not exist: ${target_name}")
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET "${target_name}" POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/data"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/BugTrapU-x64.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/libyuv.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/libmp4v2.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/openh264-2.0.0-win64.dll"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openvr/bin/win64/openvr_api.dll"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/openvr_api.dll"
|
||||
VERBATIM)
|
||||
endfunction()
|
||||
@@ -1,6 +1,85 @@
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
set(PP_LEGACY_ENGINE_SOURCES
|
||||
src/hmd.cpp
|
||||
src/log.cpp
|
||||
src/mp4enc.cpp
|
||||
src/util.cpp
|
||||
src/wacom.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_ASSETS_IO_SOURCES
|
||||
src/abr.cpp
|
||||
src/asset.cpp
|
||||
src/binary_stream.cpp
|
||||
src/image.cpp
|
||||
src/serializer.cpp
|
||||
src/settings.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
|
||||
src/action.cpp
|
||||
src/bezier.cpp
|
||||
src/brush.cpp
|
||||
src/canvas.cpp
|
||||
src/canvas_actions.cpp
|
||||
src/canvas_layer.cpp
|
||||
src/event.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_RENDERER_GL_SOURCES
|
||||
src/font.cpp
|
||||
src/rtt.cpp
|
||||
src/shader.cpp
|
||||
src/shape.cpp
|
||||
src/texture.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_UI_CORE_SOURCES
|
||||
src/layout.cpp
|
||||
src/node.cpp
|
||||
src/node_border.cpp
|
||||
src/node_button.cpp
|
||||
src/node_button_custom.cpp
|
||||
src/node_checkbox.cpp
|
||||
src/node_combobox.cpp
|
||||
src/node_icon.cpp
|
||||
src/node_image.cpp
|
||||
src/node_image_texture.cpp
|
||||
src/node_input_box.cpp
|
||||
src/node_popup_menu.cpp
|
||||
src/node_progress_bar.cpp
|
||||
src/node_remote_page.cpp
|
||||
src/node_scroll.cpp
|
||||
src/node_settings.cpp
|
||||
src/node_shorcuts.cpp
|
||||
src/node_slider.cpp
|
||||
src/node_text.cpp
|
||||
src/node_text_input.cpp
|
||||
)
|
||||
|
||||
set(PP_LEGACY_APP_SOURCES
|
||||
src/canvas_modes.cpp
|
||||
src/legacy_app_shell_services.cpp
|
||||
src/legacy_app_shell_services.h
|
||||
src/legacy_canvas_tool_services.cpp
|
||||
src/legacy_canvas_tool_services.h
|
||||
src/legacy_canvas_view_services.cpp
|
||||
src/legacy_canvas_view_services.h
|
||||
src/legacy_preference_storage.cpp
|
||||
src/legacy_preference_storage.h
|
||||
src/legacy_document_canvas_services.cpp
|
||||
src/legacy_document_canvas_services.h
|
||||
src/legacy_document_layer_services.cpp
|
||||
src/legacy_document_layer_services.h
|
||||
src/legacy_history_services.cpp
|
||||
src/legacy_history_services.h
|
||||
src/legacy_recording_services.cpp
|
||||
src/legacy_recording_services.h
|
||||
src/legacy_ui_overlay_services.cpp
|
||||
src/legacy_ui_overlay_services.h
|
||||
src/pch.cpp
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_cloud.cpp
|
||||
src/app_commands.cpp
|
||||
@@ -9,32 +88,43 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/app_layout.cpp
|
||||
src/app_shaders.cpp
|
||||
src/app_vr.cpp
|
||||
src/asset.cpp
|
||||
src/bezier.cpp
|
||||
src/binary_stream.cpp
|
||||
src/brush.cpp
|
||||
src/canvas.cpp
|
||||
src/canvas_actions.cpp
|
||||
src/canvas_layer.cpp
|
||||
src/canvas_modes.cpp
|
||||
src/event.cpp
|
||||
src/font.cpp
|
||||
src/hmd.cpp
|
||||
src/image.cpp
|
||||
src/layout.cpp
|
||||
src/log.cpp
|
||||
src/mp4enc.cpp
|
||||
src/node.cpp
|
||||
src/legacy_app_dialog_services.cpp
|
||||
src/legacy_app_dialog_services.h
|
||||
src/legacy_app_preference_services.cpp
|
||||
src/legacy_app_preference_services.h
|
||||
src/legacy_app_startup_services.cpp
|
||||
src/legacy_app_startup_services.h
|
||||
src/legacy_brush_package_import_services.cpp
|
||||
src/legacy_brush_package_import_services.h
|
||||
src/legacy_brush_package_export_services.cpp
|
||||
src/legacy_brush_package_export_services.h
|
||||
src/legacy_cloud_services.cpp
|
||||
src/legacy_cloud_services.h
|
||||
src/legacy_document_export_services.cpp
|
||||
src/legacy_document_export_services.h
|
||||
src/legacy_document_open_services.cpp
|
||||
src/legacy_document_open_services.h
|
||||
src/legacy_document_session_services.cpp
|
||||
src/legacy_document_session_services.h
|
||||
src/platform_legacy/legacy_platform_services.cpp
|
||||
src/platform_legacy/legacy_platform_services.h
|
||||
src/version.cpp
|
||||
)
|
||||
|
||||
set(PP_PANOPAINTER_UI_SOURCES
|
||||
src/legacy_brush_ui_services.cpp
|
||||
src/legacy_brush_ui_services.h
|
||||
src/legacy_document_animation_services.cpp
|
||||
src/legacy_document_animation_services.h
|
||||
src/legacy_grid_ui_services.cpp
|
||||
src/legacy_grid_ui_services.h
|
||||
src/legacy_quick_ui_services.cpp
|
||||
src/legacy_quick_ui_services.h
|
||||
src/node_about.cpp
|
||||
src/node_border.cpp
|
||||
src/node_button.cpp
|
||||
src/node_button_custom.cpp
|
||||
src/node_canvas.cpp
|
||||
src/node_changelog.cpp
|
||||
src/node_checkbox.cpp
|
||||
src/node_color_quad.cpp
|
||||
src/node_colorwheel.cpp
|
||||
src/node_combobox.cpp
|
||||
src/node_dialog_browse.cpp
|
||||
src/node_dialog_cloud.cpp
|
||||
src/node_dialog_export_ppbr.cpp
|
||||
@@ -42,10 +132,6 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/node_dialog_open.cpp
|
||||
src/node_dialog_picker.cpp
|
||||
src/node_dialog_resize.cpp
|
||||
src/node_icon.cpp
|
||||
src/node_image.cpp
|
||||
src/node_image_texture.cpp
|
||||
src/node_input_box.cpp
|
||||
src/node_message_box.cpp
|
||||
src/node_metadata.cpp
|
||||
src/node_panel_animation.cpp
|
||||
@@ -56,33 +142,19 @@ set(PP_LEGACY_APP_SOURCES
|
||||
src/node_panel_layer.cpp
|
||||
src/node_panel_quick.cpp
|
||||
src/node_panel_stroke.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_stroke_preview.cpp
|
||||
src/node_text.cpp
|
||||
src/node_text_input.cpp
|
||||
src/node_tool_bucket.cpp
|
||||
src/node_usermanual.cpp
|
||||
src/node_viewport.cpp
|
||||
src/pch.cpp
|
||||
src/rtt.cpp
|
||||
src/serializer.cpp
|
||||
src/settings.cpp
|
||||
src/shader.cpp
|
||||
src/shape.cpp
|
||||
src/texture.cpp
|
||||
src/util.cpp
|
||||
src/version.cpp
|
||||
src/wacom.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
|
||||
src/main.cpp
|
||||
PanoPainter.rc
|
||||
)
|
||||
|
||||
@@ -140,4 +212,3 @@ set(PP_LEGACY_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ function(pp_configure_project_warnings target)
|
||||
/W4
|
||||
/permissive-
|
||||
/Zc:__cplusplus
|
||||
/Zc:preprocessor)
|
||||
/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()
|
||||
@@ -15,7 +17,9 @@ function(pp_configure_project_warnings target)
|
||||
-Wpedantic
|
||||
-Wconversion
|
||||
-Wshadow
|
||||
-Wnull-dereference)
|
||||
-Wnull-dereference
|
||||
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
|
||||
-Wno-unused-parameter)
|
||||
endif()
|
||||
|
||||
if(PP_ENABLE_ASAN)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-05-31
|
||||
Last updated: 2026-06-05
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
is extracted, update the relevant rows with the owning component, test label,
|
||||
@@ -11,42 +11,45 @@ and validation command.
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture |
|
||||
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture, live-canvas-to-`pp_document` snapshot projection with captured RGBA8 payloads, pending renderer-readback counts, save-readiness reporting before retained live saves, pure PPI export from payload-complete snapshots, and shared paint-renderer export-readiness reporting from the same snapshot |
|
||||
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
|
||||
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
|
||||
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
|
||||
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
|
||||
| Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke |
|
||||
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input, destination-feedback copy/fetch gate |
|
||||
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
|
||||
| App status and renderer diagnostics | App title/status widgets, extension indicators | `pp_app_core`, `pp_renderer_api`, `pp_panopainter_ui` | Title/status text tests, renderer diagnostic indicator tests, CLI status smoke, live layout adapter smoke |
|
||||
|
||||
## Image And Export
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
|
||||
| PNG/JPEG export | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden output tolerance |
|
||||
| Equirectangular import/export | `Canvas`, shaders, RTT | `pp_paint_renderer` | Tiny cube/equirect golden |
|
||||
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
|
||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
|
||||
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests, live export-adapter document snapshot readiness through the shared paint-renderer export report, pure cube-face PNG writer, pure equirectangular PNG/JPEG+XMP writers, pure layer/frame collection PNG writers, app-core collection write executor, retained fallback coverage |
|
||||
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests, live export-adapter renderer-upload/face-PNG readiness report, pure document-frame equirectangular PNG and JPEG+XMP export with live writer fallback, pure layer/frame equirectangular PNG collection export, exact GPU/golden parity |
|
||||
| Cube face export | `Canvas` fallback | `pp_paint_renderer`, `pp_app_core` | Pure six-face document frame composite, renderer texture-upload bridge, shared export-readiness report, app-core face filename planning and write/publish service execution, payload-complete canvas-snapshot renderer-upload and face-PNG automation, live document/renderer face-PNG writer with retained Canvas fallback, OpenGL command-plan coverage, six-face golden set |
|
||||
| Depth export | `Canvas`, grid tools | `pp_paint_renderer`, `pp_app_core` | Depth target/write planning, document-snapshot renderer-readiness logging, depth render-plan draw/readback counts, retained render/readback parity, and format/golden validation |
|
||||
|
||||
## Brush And Painting
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Brush settings serialization | `Brush`, `Serializer` | `pp_paint`, `pp_assets` | Round-trip and boundary values |
|
||||
| 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` | CPU reference and GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | CPU reference vectors and GPU parity |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference, dual/pattern feedback planning, GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors, pure `pp_document` face and six-face frame compositing plus renderer texture upload/OpenGL command-plan coverage, fixed-function/framebuffer-fetch/ping-pong stroke composite planning, live `Canvas`/`NodeCanvas` blend-gate coverage, live canvas stroke/thumbnail/brush-preview destination-copy coverage, and GPU parity |
|
||||
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||
|
||||
## Layers And Animation
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Layer add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document` | Undo/redo invariant tests |
|
||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
|
||||
| Layer rename/add/remove/move/merge | `Canvas`, `Layer`, actions | `pp_document`, `pp_app_core` | Rename and operation planning, service-dispatch, no-op preservation, undo/redo invariant tests |
|
||||
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_app_core`, `pp_paint_renderer` | Metadata planning, service-dispatch, live-canvas-to-`pp_document` snapshot projection, CPU model and render golden |
|
||||
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
|
||||
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
|
||||
| MP4/timelapse export | `MP4Encoder`, recording thread | `pp_assets`, `pp_paint_renderer`, app | Smoke export and cancellation |
|
||||
| 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
|
||||
|
||||
@@ -54,8 +57,8 @@ and validation command.
|
||||
| --- | --- | --- | --- |
|
||||
| XML layout parsing | `LayoutManager`, `Node` | `pp_ui_core` | Layout fixtures and malformed XML |
|
||||
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
|
||||
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch and layout tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui` | UI automation scripts |
|
||||
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained overlay-adapter build coverage, retained popup/dialog lifetime tests |
|
||||
| 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 |
|
||||
|
||||
@@ -63,11 +66,11 @@ and validation command.
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback |
|
||||
| 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 | `App` platform methods | `pp_platform_*` | Platform smoke or mocked service |
|
||||
| Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke |
|
||||
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
|
||||
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
|
||||
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |
|
||||
| Desktop XR | `HMD`, `Vive`, `app_vr`, retained OpenVR bridge | `pp_platform_vr`, app with OpenXR backend | Runtime-selection policy tests, compile gate, and mocked pose tests |
|
||||
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
|
||||
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
|
||||
|
||||
@@ -75,9 +78,8 @@ and validation command.
|
||||
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Upload/download/browse | `app_cloud`, CURL helpers | app service, `pp_platform_*` | Mocked HTTP and timeout 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
216
docs/modernization/director-workflow.md
Normal file
216
docs/modernization/director-workflow.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Modernization Director Workflow
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-12
|
||||
|
||||
Use this workflow when the user explicitly asks for subagents, delegation, a
|
||||
director, captains, or parallel agent work. Do not spawn subagents just because
|
||||
a task is complex. The default is still one agent executing one task from
|
||||
`docs/modernization/tasks.md`.
|
||||
|
||||
## Goals
|
||||
|
||||
- Save main-thread tokens by keeping long exploration and simple edits out of
|
||||
the director context.
|
||||
- Use stronger models only where they change the outcome.
|
||||
- Keep each implementation slice measurable, validated, committed, and pushed.
|
||||
- Avoid merge conflicts by giving every agent a disjoint task and file scope.
|
||||
- Keep teams rolling: when one team finishes, integrate or park its result and
|
||||
start the next disjoint team without waiting for every other team to finish.
|
||||
|
||||
## Roles
|
||||
|
||||
### Director
|
||||
|
||||
The director is the main agent in the user thread. The director owns:
|
||||
|
||||
- choosing the task group from `docs/modernization/tasks.md`
|
||||
- deciding whether delegation is worth the coordination cost
|
||||
- spawning one `gpt-5.4` captain per independent task group, including
|
||||
multiple teams when scopes are disjoint
|
||||
- integrating returned changes
|
||||
- running final validation
|
||||
- updating docs/debt/tasks
|
||||
- committing and pushing the verified slice
|
||||
- resetting conversation context after the slice when practical
|
||||
|
||||
The director should keep local work minimal. It should not implement production
|
||||
changes unless a required integration fix is smaller than another delegation
|
||||
round. Prefer giving captains ownership of implementation, focused validation,
|
||||
and worker coordination, then use the director context only for scope selection,
|
||||
conflict review, final validation, docs/task updates, commits, and pushes.
|
||||
|
||||
The director may do a quick local blocking check before delegation, such as
|
||||
reading task rows, checking git status, or confirming that two scopes do not
|
||||
overlap. If the next action is substantive code or test work, delegate it to a
|
||||
captain or worker whenever the scope can be made clear.
|
||||
|
||||
### Team Captain
|
||||
|
||||
Use `gpt-5.4` for each captain. A captain owns one coherent task group, for
|
||||
example:
|
||||
|
||||
- renderer/export boundary
|
||||
- legacy adapter retirement
|
||||
- platform/package parity
|
||||
- UI lifetime
|
||||
- dependency cleanup
|
||||
- test/hardening work
|
||||
|
||||
The captain owns implementation for its assigned task group. A team should be a
|
||||
captain plus multiple workers when the work can be split into disjoint files.
|
||||
The captain turns one task-group objective, or a small coherent run of adjacent
|
||||
task rows, into smaller disjoint subtasks, spawns or requests workers when
|
||||
useful, reviews their changes, runs focused validation when cheap, and returns
|
||||
an integration-ready result. If nested subagents are available, the captain may
|
||||
spawn workers and continue through the next compatible subtask after each worker
|
||||
returns. If nested subagents are not available in the current surface, the
|
||||
captain returns a worker plan and the director performs the second-level spawns
|
||||
without taking over implementation.
|
||||
|
||||
The captain must not edit broad shared files unless assigned. The captain must
|
||||
not rewrite the task tracker or roadmap except for a requested status note.
|
||||
|
||||
### Workers And Explorers
|
||||
|
||||
Workers perform bounded edits in assigned files. Explorers answer specific
|
||||
questions and should usually not edit files.
|
||||
|
||||
Every worker and explorer must be told:
|
||||
|
||||
- this repository may have other agents working in parallel
|
||||
- do not revert or overwrite unrelated changes
|
||||
- stay inside the assigned scope
|
||||
- report changed files, validation run, and blockers
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Work Type | Model | Reasoning Effort | Use |
|
||||
| --- | --- | --- | --- |
|
||||
| Captain for a task group | `gpt-5.4` | `medium` default, `high` for renderer/platform architecture | Split task, supervise workers, summarize integration. |
|
||||
| Risky implementation with cross-file C++ behavior | `gpt-5.4` | `medium` or `high` | Code changes touching contracts, build graph, or live adapters. |
|
||||
| Bounded implementation in known files | `gpt-5.4-mini` | `medium` | Localized tests, simple adapters, docs/debt updates, small refactors. |
|
||||
| Fast lookup or inventory | `gpt-5.3-codex-spark` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
|
||||
| Mechanical docs cleanup | `gpt-5.3-codex-spark` | `low` | Formatting, table updates, command list normalization. |
|
||||
| Ambiguous strategy or failed captain escalation | `gpt-5.5` | inherited or `high` | Only when cheaper models are likely to make architectural mistakes. |
|
||||
|
||||
Prefer the inherited model unless the user requested this director workflow or
|
||||
there is a clear task-specific reason to override. In this workflow, the user
|
||||
has requested `gpt-5.4` captains, so use that override for captains.
|
||||
|
||||
## Token Discipline
|
||||
|
||||
- Use `fork_context=false` by default. Pass the task id, relevant files, debt
|
||||
ids, validation commands, and only the necessary excerpts.
|
||||
- Use `fork_context=true` only when prior conversation details are essential and
|
||||
not already in repo docs.
|
||||
- Do not paste large logs into prompts. Point agents at log paths and ask for
|
||||
the smallest relevant excerpt.
|
||||
- Do not ask captains or workers to "read the roadmap" generally. Name the
|
||||
exact task row and debt ids.
|
||||
- Keep subagent prompts under about 250 words for explorers and 500 words for
|
||||
workers/captains unless the task truly needs more.
|
||||
- Ask for compact final reports: changed files, result, validation, blockers,
|
||||
next recommendation.
|
||||
- Close completed agents after their results are integrated or rejected.
|
||||
- Prefer the smallest number of teams that keeps disjoint work moving. Multiple
|
||||
captains are appropriate when task rows have non-overlapping write scopes and
|
||||
can validate independently. Avoid many agents in one file family.
|
||||
- Do not synchronize all teams at a barrier unless validation or merge risk
|
||||
requires it. Use rolling integration: wait for whichever team finishes first,
|
||||
process that result, then immediately start another disjoint team if ready
|
||||
work remains.
|
||||
|
||||
## Delegation Flow
|
||||
|
||||
1. Director picks two or more `Ready` tasks from
|
||||
`docs/modernization/tasks.md` with disjoint write scopes.
|
||||
2. Director assigns each independent task group to a `gpt-5.4` captain.
|
||||
3. Captains implement directly or coordinate workers/explorers for disjoint
|
||||
subtasks, and may continue through a coherent sequence of adjacent tasks
|
||||
within the assigned scope.
|
||||
4. Captains run focused validation when cheap and return changed files,
|
||||
validation, blockers, and integration notes.
|
||||
5. Director waits for whichever team finishes first, reviews for scope
|
||||
conflicts, integrates returned changes, and starts the next disjoint team
|
||||
while other teams keep running.
|
||||
6. Director runs the listed validation command or the quiet checkpoint wrapper
|
||||
for each integrated slice.
|
||||
7. Director updates `tasks.md`, `debt.md`, and `roadmap.md` if tasks moved.
|
||||
8. Director commits and pushes verified slices incrementally.
|
||||
|
||||
## Director Prompt Template For A Captain
|
||||
|
||||
```text
|
||||
You are the gpt-5.4 team captain for the <group> task group in PanoPainter.
|
||||
|
||||
Task source: docs/modernization/tasks.md task(s) <TASK-ID-LIST>.
|
||||
Debt ids: <DEBT-LIST>.
|
||||
Assigned scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
|
||||
Goal: <ONE PARAGRAPH>.
|
||||
|
||||
Own this task group through implementation. Build a small team of workers for
|
||||
disjoint subtasks when possible, review their outputs, and keep looping through
|
||||
the assigned coherent task sequence until the scope is done, blocked, or no
|
||||
longer safe to continue. Use gpt-5.4 only for risky C++ behavior changes. Keep
|
||||
tasks small enough to validate. Do not edit outside the assigned scope. Other
|
||||
agents may be working in parallel; do not revert unrelated changes.
|
||||
|
||||
Return:
|
||||
- recommended worker tasks with model choice and file scope
|
||||
- completed edits or the exact blocker preventing them
|
||||
- focused validation run and result
|
||||
- risks/blockers
|
||||
- integration notes for the director
|
||||
```
|
||||
|
||||
## Captain Prompt Template For A Worker
|
||||
|
||||
```text
|
||||
You are a worker on PanoPainter. Other agents may be editing nearby files; do
|
||||
not revert unrelated changes.
|
||||
|
||||
Task: <WORKER-TASK>.
|
||||
Model fit: <WHY THIS MODEL IS ENOUGH>.
|
||||
Write scope: <FILES/DIRS ONLY>.
|
||||
Read scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
|
||||
Make the smallest behavior-preserving change that satisfies the done checks.
|
||||
Update tests/docs only if listed. If the task is larger than expected, stop and
|
||||
report the split instead of broadening scope.
|
||||
|
||||
Final report:
|
||||
- files changed
|
||||
- behavior changed
|
||||
- validation run and result
|
||||
- blockers or follow-up
|
||||
```
|
||||
|
||||
## Explorer Prompt Template
|
||||
|
||||
```text
|
||||
Answer one codebase question for PanoPainter.
|
||||
|
||||
Question: <QUESTION>.
|
||||
Search scope: <FILES/DIRS>.
|
||||
Use compiler-aware navigation if this depends on C++ symbols.
|
||||
Do not edit files.
|
||||
|
||||
Return only:
|
||||
- answer
|
||||
- supporting file references
|
||||
- confidence and caveats
|
||||
```
|
||||
|
||||
## Final Integration Checklist
|
||||
|
||||
- No worker changed files outside its assigned scope without calling it out.
|
||||
- No generated logs or build output were committed.
|
||||
- Focused validation for the task passed or the failure is documented.
|
||||
- `docs/modernization/debt.md` changed when debt was narrowed or closed.
|
||||
- `docs/modernization/tasks.md` score changed only for `Done` tasks.
|
||||
- The commit contains one coherent slice.
|
||||
- The branch was pushed.
|
||||
File diff suppressed because it is too large
Load Diff
569
docs/modernization/tasks.md
Normal file
569
docs/modernization/tasks.md
Normal file
@@ -0,0 +1,569 @@
|
||||
# Modernization Task Tracker
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-12
|
||||
|
||||
This file turns the modernization roadmap into small, measurable work items.
|
||||
The roadmap explains direction, the debt log explains why shortcuts remain, and
|
||||
this tracker is the execution queue. Prefer closing one task here over adding a
|
||||
new broad roadmap paragraph.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Pick one `Ready` task at a time unless the user asks for planning only.
|
||||
- Keep each slice small enough to validate and commit in one session.
|
||||
- Do not claim percentage progress for "narrowed" debt. Points move only when a
|
||||
task row is changed to `Done`.
|
||||
- A task is `Done` only when its listed checks pass, the debt log is updated or
|
||||
closed as applicable, and the roadmap/task score is updated.
|
||||
- If a task proves too large, split it before editing code. The original task
|
||||
stays `Ready` or becomes `Blocked` with the reason.
|
||||
- After a verified task is committed and pushed, reset conversation context
|
||||
before starting the next task when practical.
|
||||
- When the user asks for subagents or delegation, follow
|
||||
`docs/modernization/director-workflow.md` and keep each delegated task mapped
|
||||
to a row in this tracker.
|
||||
|
||||
## Progress Scorecard
|
||||
|
||||
The current score is intentionally conservative. It should move in visible,
|
||||
auditable steps rather than by subjective estimates.
|
||||
|
||||
| Area | Weight | Current | Progress Rule |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Build and CMake ownership | 15 | 13 | Root CMake owns active source lists, app/tool targets, and retained package entrypoints. |
|
||||
| Test and automation coverage | 15 | 9 | Headless, platform, package, and focused validation commands exist and are current. |
|
||||
| Pure component behavior ownership | 15 | 8 | Behavior lives in `pp_*` components and is consumed by live adapters. |
|
||||
| Legacy adapter retirement | 20 | 7 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. |
|
||||
| Renderer boundary and OpenGL parity | 15 | 10 | Live render/export/readback paths execute through renderer interfaces with parity checks. |
|
||||
| Platform and package parity | 10 | 6 | Required platforms have root CMake/package validation and injected platform services. |
|
||||
| Hardening and future backend readiness | 10 | 2 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. |
|
||||
| **Total** | **100** | **55** | Only completed tasks below may change this number. |
|
||||
|
||||
When updating `Current`, add a dated note under "Completed Task Log" with the
|
||||
task id, points moved, validation command, and commit hash.
|
||||
|
||||
## Task States
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| `Ready` | Clear enough for an agent to execute. |
|
||||
| `In progress` | Actively being changed in the current slice. |
|
||||
| `Blocked` | Needs a user decision, missing toolchain, or a prior task. |
|
||||
| `Done` | Validated, documented, committed, and pushed. |
|
||||
|
||||
## Ready Queue
|
||||
|
||||
### MT-001 - Adopt Measurable Task Tracking
|
||||
|
||||
Status: Done
|
||||
Score: no score movement
|
||||
Debt: none
|
||||
Scope: `docs/modernization/tasks.md`, `docs/modernization/roadmap.md`
|
||||
|
||||
Steps:
|
||||
|
||||
- Add this tracker.
|
||||
- Link it from the roadmap.
|
||||
- Make the scorecard the source for percentage claims.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `docs/modernization/roadmap.md` points agents to this file.
|
||||
- The tracker has task states, scoring rules, and at least one ready queue.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
git diff -- docs\modernization\roadmap.md docs\modernization\tasks.md
|
||||
```
|
||||
|
||||
### ADP-001 - Remove History Bridge From Document Resize And Canvas Clear
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0020`, `DEBT-0027`
|
||||
Scope: `src/legacy_document_canvas_services.*`,
|
||||
`src/app_core/document_resize.h`, `tests/app_core/document_resize_tests.cpp`,
|
||||
related canvas-clear tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Make document resize and canvas-clear execution consume app-core history
|
||||
commands directly instead of routing through `legacy_history_services`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_canvas_services.*` no longer includes
|
||||
`legacy_history_services.h`.
|
||||
- Resize still executes in order: resize, title update, history clear.
|
||||
- Canvas clear still records undo and marks the document unsaved when a canvas
|
||||
exists.
|
||||
- `docs/modernization/debt.md` narrows or closes the affected removal condition.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_resize|pp_app_core_document_canvas|pano_cli_plan_document_resize|pano_cli_plan_canvas_clear" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-002 - Remove History Bridge From Layer Operations
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0021`
|
||||
Scope: `src/legacy_document_layer_services.*`,
|
||||
`src/app_core/document_layer.h`, `tests/app_core/document_layer_tests.cpp`
|
||||
|
||||
Goal:
|
||||
|
||||
Move layer add/remove/merge/clear/rename history side effects into tested
|
||||
app-core execution plans so the live layer bridge no longer calls
|
||||
`legacy_history_services`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_layer_services.*` no longer includes
|
||||
`legacy_history_services.h`.
|
||||
- Layer operations still preserve undo/history behavior covered by
|
||||
`pp_app_core_document_layer_tests`.
|
||||
- `pano_cli plan-layer-operation`, `plan-layer-menu`, and
|
||||
`plan-layer-rename` JSON remains compatible.
|
||||
- `docs/modernization/debt.md` records the narrowed or closed layer-history
|
||||
adapter dependency.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer|pano_cli_plan_layer" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-003 - Remove History Bridge From Document Open And Session Save
|
||||
|
||||
Status: Done
|
||||
Score: +1 legacy adapter retirement
|
||||
Debt: `DEBT-0039`, `DEBT-0040`, `DEBT-0042`
|
||||
Scope: `src/legacy_document_open_services.*`,
|
||||
`src/legacy_document_session_services.*`,
|
||||
`src/app_core/document_session.*`, `src/app_core/document_route.*`,
|
||||
matching tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Make document-open, close, save, save-before-workflow, Save As, and Save Version
|
||||
history effects explicit app-core outputs instead of direct
|
||||
`legacy_history_services` calls in the live bridges.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/legacy_document_open_services.*` and
|
||||
`src/legacy_document_session_services.*` no longer include
|
||||
`legacy_history_services.h`.
|
||||
- Existing dirty-document, save-before, new-document, Save As, and Save Version
|
||||
plans preserve their JSON contracts.
|
||||
- The debt log is updated for every debt id listed above.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_route|pp_app_core_document_session|pano_cli_plan_open_route|pano_cli_simulate_app_session|pano_cli_plan_document_file|pano_cli_plan_document_version" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-004 - Make Dialog Creation A UI Factory Boundary
|
||||
|
||||
Status: Done
|
||||
Score: +2 legacy adapter retirement
|
||||
Debt: `DEBT-0058`, `DEBT-0063`
|
||||
Scope: `src/legacy_app_dialog_services.*`,
|
||||
`src/legacy_ui_overlay_services.*`, `src/app_dialogs.cpp`,
|
||||
`src/app_core/app_dialog.h`, dialog tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Keep app-core dialog metadata pure, but move retained
|
||||
`NodeProgressBar`/`NodeMessageBox`/`NodeInputBox` construction behind one
|
||||
`pp_panopainter_ui` or retained UI factory function. `App` should ask for a
|
||||
dialog object through an interface instead of knowing individual node creation
|
||||
details.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `App::show_progress`, `App::message_box`, and `App::input_box` still preserve
|
||||
captions, cancel behavior, and keyboard behavior.
|
||||
- New factory path has focused tests or existing `pp_app_core_app_dialog_tests`
|
||||
plus a smoke command proving the live adapter still builds.
|
||||
- The debt log states exactly which raw-node lifetime hazards remain.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_app_dialog|pano_cli_plan_app_dialog" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### ADP-005 - Convert One Popup/Dialog Family To Checked Overlay Lifetime
|
||||
|
||||
Status: Done
|
||||
Score: +2 legacy adapter retirement
|
||||
Debt: `DEBT-0063`
|
||||
Scope: choose exactly one family from `src/node_dialog_open.cpp`,
|
||||
`src/node_dialog_browse.cpp`, `src/node_panel_quick.cpp`,
|
||||
`src/node_panel_stroke.cpp`, or `src/node_combobox.cpp`, plus
|
||||
`src/legacy_ui_overlay_services.*`
|
||||
|
||||
Goal:
|
||||
|
||||
Adopt `pp_ui_core` overlay lifetime semantics for one retained popup/dialog
|
||||
family before trying to rewrite all retained UI nodes.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- The chosen family no longer owns open-coded root insertion, outside-click
|
||||
release, close callback wiring, or destroy-during-callback behavior.
|
||||
- Existing UI behavior is preserved.
|
||||
- Tests cover missing root/template handling and close/release behavior for the
|
||||
chosen family.
|
||||
- The debt log names the completed family and the remaining families.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_ui_core_overlay_lifetime|pp_ui_core_node_lifetime" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||
```
|
||||
|
||||
### RND-001 - Make Pure Equirectangular Export The Primary Success Path
|
||||
|
||||
Status: Done
|
||||
Score: +2 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/legacy_document_export_services.*`,
|
||||
`src/app_core/document_export.*`, `src/paint_renderer/compositor.*`,
|
||||
`tests/app_core/document_export_tests.cpp`,
|
||||
`tests/paint_renderer/compositor_tests.cpp`
|
||||
|
||||
Goal:
|
||||
|
||||
For payload-complete snapshots, live PNG/JPEG equirectangular export should
|
||||
complete through the pure document/paint-renderer writer. Retained
|
||||
`Canvas::export_equirectangular*` should run only for unsupported Web,
|
||||
incomplete-readback, or writer-failure fallbacks.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Export route reports whether the pure writer was used or why fallback was
|
||||
required.
|
||||
- Tests cover pure-writer success and each fallback reason.
|
||||
- Live bridge does not call retained equirectangular export after pure-writer
|
||||
success.
|
||||
- The debt log narrows retained equirectangular export execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route|pano_cli_simulate_document_export" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-002 - Make Pure Layer And Animation Collection Export Primary
|
||||
|
||||
Status: Done
|
||||
Score: +2 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/legacy_document_export_services.*`,
|
||||
`src/app_core/document_export.*`, `src/paint_renderer/compositor.*`,
|
||||
collection export tests only
|
||||
|
||||
Goal:
|
||||
|
||||
For payload-complete snapshots, live layer and animation-frame collection export
|
||||
should complete through the pure collection writer. Retained
|
||||
`Canvas::export_layers*` and `Canvas::export_anim_frames*` should run only for
|
||||
unsupported Web, incomplete-readback, or writer-failure fallbacks.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Collection export route reports pure-writer success versus fallback reason.
|
||||
- Tests cover layer collection, animation-frame collection, and fallback.
|
||||
- The debt log narrows retained collection export execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-003 - Replace Depth Export Readiness With Pure Depth Export Execution
|
||||
|
||||
Status: Done
|
||||
Score: +3 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0010`, `DEBT-0036`, `DEBT-0043`
|
||||
Scope: `src/paint_renderer/compositor.*`,
|
||||
`src/app_core/document_export.*`, `src/legacy_document_export_services.*`,
|
||||
depth tests only
|
||||
|
||||
Goal:
|
||||
|
||||
Turn the current pure depth export render plan into actual payload generation
|
||||
for payload-complete snapshots, then write image/depth payloads through the
|
||||
existing app-core two-payload writer before falling back to retained
|
||||
`Canvas::export_depth*`.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Pure depth export produces deterministic image and depth payloads for a
|
||||
payload-complete snapshot.
|
||||
- Retained depth export runs only for unsupported targets, incomplete readback,
|
||||
or writer failure.
|
||||
- Tests cover malformed depth target inputs and byte-size validation.
|
||||
- The debt log narrows depth export readback/execution.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export|pp_paint_renderer_compositor|pano_cli_plan_export_snapshot_route" --output-on-failure
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pano_cli
|
||||
```
|
||||
|
||||
### RND-004 - Add First Desktop GPU Golden Gate
|
||||
|
||||
Status: Done
|
||||
Score: +2 hardening and future backend readiness
|
||||
Debt: `DEBT-0036`
|
||||
Scope: `tests/`, `CMakeLists.txt`, renderer test helpers only
|
||||
|
||||
Goal:
|
||||
|
||||
Create the first non-default `desktop-gpu` golden/readback test that validates
|
||||
one OpenGL output against a deterministic fixture. Keep it opt-in so headless
|
||||
agents are not blocked.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `ctest --preset desktop-gpu --build-config Debug` has at least one real test.
|
||||
- The test is skipped with a clear message when no GPU/context is available.
|
||||
- The roadmap and debt log describe what the golden covers and what remains.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-gpu --build-config Debug --output-on-failure
|
||||
ctest --preset desktop-fast --build-config Debug -R "pp_renderer_gl|pp_paint_renderer" --output-on-failure
|
||||
```
|
||||
|
||||
### PLT-001 - Split Apple Picker/Browse Service From Legacy Platform Adapter
|
||||
|
||||
Status: Done
|
||||
Score: +2 platform and package parity
|
||||
Debt: `DEBT-0017`, `DEBT-0051`, `DEBT-0055`
|
||||
Scope: `src/platform_legacy/legacy_platform_services.*`, new
|
||||
`src/platform_apple/*` files if needed, `CMakeLists.txt`, platform API tests
|
||||
|
||||
Goal:
|
||||
|
||||
Move macOS/iOS document browse roots, file picking, directory picking, and
|
||||
display-path formatting out of the catch-all legacy platform adapter and into a
|
||||
named Apple platform service boundary.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/platform_legacy/legacy_platform_services.*` no longer owns Apple
|
||||
browse/picker policy.
|
||||
- Apple compile validation still passes through the remote build script.
|
||||
- The debt log narrows Apple platform shell extraction.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device
|
||||
```
|
||||
|
||||
### PLT-002 - Split Web Export/Storage Policy From Legacy Platform Adapter
|
||||
|
||||
Status: Done
|
||||
Score: +2 platform and package parity
|
||||
Debt: `DEBT-0050`, `DEBT-0053`, `DEBT-0057`
|
||||
Scope: `src/platform_legacy/legacy_platform_services.*`, new
|
||||
`src/platform_web/*` files if needed, `src/platform_api/*`, platform tests
|
||||
|
||||
Goal:
|
||||
|
||||
Move WebGL exported-image publishing, persistent-storage flushing,
|
||||
prepared-file handoff, and default canvas resolution out of the catch-all
|
||||
legacy platform adapter into a named Web platform service boundary.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Web policy is injectable through `pp_platform_api`.
|
||||
- The legacy adapter no longer owns Web default canvas resolution or storage
|
||||
flush policy.
|
||||
- Package-smoke readiness still reports Web blockers explicitly.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly
|
||||
```
|
||||
|
||||
### DEP-001 - Remove Generated fmt Overlay
|
||||
|
||||
Status: Done
|
||||
Score: +1 build and CMake ownership
|
||||
Debt: `DEBT-0062`
|
||||
Scope: `CMakeLists.txt`, `cmake/`, `vcpkg.json`, `libs/fmt` or package wiring
|
||||
|
||||
Goal:
|
||||
|
||||
Use a supported fmt package or update the vendored fmt release so VS 2026 no
|
||||
longer needs a generated `format.h` overlay.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- No build-tree fmt header overlay is generated.
|
||||
- `DEBT-0062` is closed.
|
||||
- Windows app and at least one focused component test build pass.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter pp_platform_api_tests
|
||||
ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure
|
||||
```
|
||||
|
||||
### DEP-002 - Remove Generated nanort Overlay
|
||||
|
||||
Status: Done
|
||||
Score: +1 build and CMake ownership
|
||||
Debt: `DEBT-0060`
|
||||
Scope: retained Android package CMake, `libs/nanort`, grid/lightmap dependency
|
||||
wiring
|
||||
Goal:
|
||||
|
||||
Update, replace, or isolate `nanort` so Android package builds do not generate
|
||||
a patched vendor overlay.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Android retained package CMake no longer generates a patched `nanort.h`.
|
||||
- `DEBT-0060` is closed.
|
||||
- Standard Android retained package validation passes.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard
|
||||
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
|
||||
```
|
||||
|
||||
## Blocked Or Later Queue
|
||||
|
||||
These are real goals, but they should not be picked until prerequisite tasks
|
||||
above have reduced risk.
|
||||
|
||||
### LATER-001 - Replace OpenVR With OpenXR
|
||||
|
||||
Status: Blocked
|
||||
Score: +3 platform and package parity
|
||||
Debt: `DEBT-0061`
|
||||
Blocked By: OpenXR package decision and runtime availability
|
||||
|
||||
Done Checks:
|
||||
|
||||
- OpenXR SDK/package target exists.
|
||||
- Windows platform service can start/stop desktop XR without OpenVR.
|
||||
- `libs/openvr` and `openvr_api.dll` deployment are removed.
|
||||
- `DEBT-0061` is closed.
|
||||
|
||||
### LATER-002 - Remove Catch2 Harness Debt
|
||||
|
||||
Status: Blocked
|
||||
Score: +2 test and automation coverage
|
||||
Debt: `DEBT-0005`
|
||||
Blocked By: vcpkg/toolchain reliability across Windows and headless presets
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Existing local test harness is replaced or permanently justified.
|
||||
- Catch2 tests run through `desktop-fast` and vcpkg headless presets.
|
||||
- `DEBT-0005` is closed.
|
||||
|
||||
### LATER-003 - Live Stroke Rasterization Through Renderer Services
|
||||
|
||||
Status: Ready
|
||||
Score: +5 renderer boundary and OpenGL parity
|
||||
Debt: `DEBT-0036`
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Live stroke rasterization, dual-brush compositing, and pattern feedback choose
|
||||
paths through renderer services.
|
||||
- OpenGL output parity is covered by golden/readback tests.
|
||||
- Retained stroke OpenGL execution is deleted or isolated as an OpenGL backend
|
||||
implementation.
|
||||
|
||||
### LATER-004 - Remove Catch-All Platform Legacy Adapter
|
||||
|
||||
Status: Blocked
|
||||
Score: +5 platform and package parity
|
||||
Debt: `DEBT-0017`
|
||||
Blocked By: Apple/Web split tasks and per-platform package validation
|
||||
|
||||
Done Checks:
|
||||
|
||||
- `src/platform_legacy/legacy_platform_services.*` is deleted or contains only
|
||||
compile-time unsupported stubs with debt entries.
|
||||
- Windows, Apple, Android, Linux, and Web platform services are named targets.
|
||||
- Platform-build and package-smoke report explicit pass/fail per platform.
|
||||
|
||||
## Completed Task Log
|
||||
|
||||
| Date | Task | Score Change | Validation | Commit |
|
||||
| --- | --- | ---: | --- | --- |
|
||||
| 2026-06-12 | RND-004 | +2 hardening and future backend readiness | `ctest --preset desktop-gpu --build-config Debug --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_renderer_gl\|pp_paint_renderer" --output-on-failure` | e37b2929 |
|
||||
| 2026-06-12 | DEP-002 | +1 build and CMake ownership | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | 648404ee |
|
||||
| 2026-06-12 | RND-002 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | RND-001 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | ADP-004 | +2 legacy adapter retirement | VS-bundled CMake build of `pp_app_core_app_dialog_tests` and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_app_dialog\|pano_cli_plan_app_dialog" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | PLT-001 | +2 platform and package parity | VS-bundled CMake build of `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; Apple remote build blocked by unpublished fmt submodule pointer before DEP-001 correction | 46fb8ef |
|
||||
| 2026-06-12 | RND-003 | +3 renderer boundary and OpenGL parity | VS-bundled CMake build of `pp_paint_renderer_compositor_tests`, `pp_app_core_document_export_tests`, and `pano_cli`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route" --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | DEP-001 | +1 build and CMake ownership | VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | 46fb8ef |
|
||||
| 2026-06-12 | ADP-003 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_route\|pp_app_core_document_session\|pano_cli_plan_open_route\|pano_cli_simulate_app_session\|pano_cli_plan_document_file\|pano_cli_plan_document_version" --output-on-failure` | 34a9e910 |
|
||||
| 2026-06-12 | PLT-002 | +2 platform and package parity | `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | 8cd38401 |
|
||||
| 2026-06-12 | ADP-002 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer" --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_layer\|pano_cli_plan_layer\|pp_platform_api_tests" --output-on-failure` | ae242852 |
|
||||
| 2026-06-12 | ADP-001 | +1 legacy adapter retirement | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_resize\|pp_app_core_document_canvas\|pano_cli_plan_document_resize\|pano_cli_plan_canvas_clear" --output-on-failure`; `powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core\|pano_cli_plan"` | e489b1e2 |
|
||||
| 2026-06-12 | MT-001 | 0 | `git diff -- docs\modernization\roadmap.md docs\modernization\tasks.md` | same docs slice |
|
||||
|
||||
## Task Template
|
||||
|
||||
Use this shape when adding a new task:
|
||||
|
||||
````markdown
|
||||
### AREA-000 - Imperative Task Name
|
||||
|
||||
Status: Ready
|
||||
Score: +N scorecard area
|
||||
Debt: `DEBT-0000`
|
||||
Scope: exact files or directories
|
||||
|
||||
Goal:
|
||||
|
||||
One paragraph describing the behavior or ownership change.
|
||||
|
||||
Done Checks:
|
||||
|
||||
- Binary, grep-able, or testable condition.
|
||||
- Debt log update condition.
|
||||
- Validation condition.
|
||||
|
||||
Validation:
|
||||
|
||||
```powershell
|
||||
command
|
||||
```
|
||||
````
|
||||
@@ -1,7 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
project(panopainter)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(PanoPainterLinux LANGUAGES C CXX)
|
||||
|
||||
add_executable(panopainter
|
||||
src/main.cpp
|
||||
@@ -120,4 +118,7 @@ target_include_directories(panopainter PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(panopainter glfw curl GL dl X11 pthread)
|
||||
target_compile_features(panopainter PRIVATE cxx_std_23)
|
||||
set_target_properties(panopainter PROPERTIES
|
||||
CXX_EXTENSIONS OFF)
|
||||
target_compile_definitions(panopainter PUBLIC "$<$<CONFIG:DEBUG>:_DEBUG>")
|
||||
|
||||
@@ -19,16 +19,25 @@ if ($NoApp) {
|
||||
& 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
|
||||
|
||||
@@ -44,6 +53,10 @@ $elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
name = "shader-validation"
|
||||
exitCode = $shaderExitCode
|
||||
},
|
||||
[ordered]@{
|
||||
name = "renderer-boundary"
|
||||
exitCode = $rendererBoundaryExitCode
|
||||
}
|
||||
)
|
||||
elapsedMs = $elapsed
|
||||
|
||||
@@ -1,20 +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}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$elapsed_ms"
|
||||
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s},{"name":"renderer-boundary","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$renderer_boundary_exit_code" "$elapsed_ms"
|
||||
exit "$exit_code"
|
||||
|
||||
103
scripts/automation/android-legacy-package-build.ps1
Normal file
103
scripts/automation/android-legacy-package-build.ps1
Normal file
@@ -0,0 +1,103 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Packages = @("standard"),
|
||||
[switch]$ConfigureOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. "$PSScriptRoot\android-sdk-env.ps1"
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
$Packages = @(Expand-ArgumentList -Values $Packages)
|
||||
|
||||
$toolchain = Set-AndroidSdkToolchainEnvironment
|
||||
$packageMap = @{
|
||||
standard = "android/android"
|
||||
quest = "android/quest"
|
||||
focus = "android/focus"
|
||||
}
|
||||
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($package in $Packages) {
|
||||
if (!$packageMap.ContainsKey($package)) {
|
||||
throw "Unknown Android package '$package'. Expected one of: standard, quest, focus."
|
||||
}
|
||||
|
||||
$sourceDir = $packageMap[$package]
|
||||
$buildDir = "out/build/android-legacy-$package-arm64"
|
||||
$toolchainFile = Join-Path $toolchain.ndkPath "build\cmake\android.toolchain.cmake"
|
||||
|
||||
$configureArgs = @(
|
||||
"-S", $sourceDir,
|
||||
"-B", $buildDir,
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$toolchainFile",
|
||||
"-DANDROID_ABI=arm64-v8a",
|
||||
"-DANDROID_PLATFORM=android-23"
|
||||
)
|
||||
|
||||
& $toolchain.cmakeCommand @configureArgs
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
if ($configureExitCode -ne 0) {
|
||||
if ($overallExitCode -eq 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
}
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "configure"
|
||||
exitCode = $configureExitCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ($ConfigureOnly) {
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "configure"
|
||||
exitCode = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
& $toolchain.cmakeCommand --build $buildDir --target native-lib
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
package = $package
|
||||
stage = "build"
|
||||
target = "native-lib"
|
||||
exitCode = $buildExitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "android-legacy-package-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidToolchain = $toolchain
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
exit $overallExitCode
|
||||
212
scripts/automation/android-sdk-env.ps1
Normal file
212
scripts/automation/android-sdk-env.ps1
Normal file
@@ -0,0 +1,212 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-AndroidSdkRoot {
|
||||
$candidates = @(
|
||||
$env:ANDROID_SDK_ROOT,
|
||||
$env:ANDROID_HOME,
|
||||
(Join-Path $env:LOCALAPPDATA "Android\Sdk")
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Container)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
throw "Android SDK root was not found. Install command-line tools or set ANDROID_SDK_ROOT."
|
||||
}
|
||||
|
||||
function Get-LatestAndroidSdkPackageDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName
|
||||
)
|
||||
|
||||
$packageRoot = Join-Path $SdkRoot $PackageName
|
||||
if (!(Test-Path -LiteralPath $packageRoot -PathType Container)) {
|
||||
throw "Android SDK package directory not found: $packageRoot"
|
||||
}
|
||||
|
||||
$packages = @(Get-ChildItem -LiteralPath $packageRoot -Directory |
|
||||
Where-Object { $_.Name -match '^\d+(\.\d+)*$' } |
|
||||
Sort-Object { [version]$_.Name } -Descending)
|
||||
|
||||
if ($packages.Count -eq 0) {
|
||||
throw "No installed Android SDK package versions found under $packageRoot"
|
||||
}
|
||||
|
||||
return $packages[0]
|
||||
}
|
||||
|
||||
function Get-AndroidSdkManagerCommand {
|
||||
param([Parameter(Mandatory=$true)][string]$SdkRoot)
|
||||
|
||||
$candidates = @(
|
||||
(Join-Path $SdkRoot "cmdline-tools\latest\bin\sdkmanager.bat"),
|
||||
(Join-Path $SdkRoot "cmdline-tools\latest\bin\sdkmanager.exe"),
|
||||
(Join-Path $SdkRoot "tools\bin\sdkmanager.bat"),
|
||||
(Join-Path $SdkRoot "tools\bin\sdkmanager.exe")
|
||||
)
|
||||
|
||||
$cmdlineToolsRoot = Join-Path $SdkRoot "cmdline-tools"
|
||||
if (Test-Path -LiteralPath $cmdlineToolsRoot -PathType Container) {
|
||||
$toolVersions = @(Get-ChildItem -LiteralPath $cmdlineToolsRoot -Directory |
|
||||
Where-Object { $_.Name -ne "latest" } |
|
||||
Sort-Object {
|
||||
try { [version]$_.Name } catch { [version]"0.0" }
|
||||
} -Descending)
|
||||
|
||||
foreach ($toolVersion in $toolVersions) {
|
||||
$candidates += (Join-Path $toolVersion.FullName "bin\sdkmanager.bat")
|
||||
$candidates += (Join-Path $toolVersion.FullName "bin\sdkmanager.exe")
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
$pathCommand = Get-Command "sdkmanager" -ErrorAction SilentlyContinue
|
||||
if ($pathCommand) {
|
||||
return $pathCommand.Source
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-LatestAvailableAndroidSdkPackageVersion {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$SdkManagerCommand,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName
|
||||
)
|
||||
|
||||
$output = @(& $SdkManagerCommand "--sdk_root=$SdkRoot" "--list" 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
throw "sdkmanager --list failed while checking $PackageName packages: $($output -join [Environment]::NewLine)"
|
||||
}
|
||||
|
||||
$versions = @()
|
||||
$pattern = "^\s*$([regex]::Escape($PackageName));([0-9]+(?:\.[0-9]+)*)\s*\|"
|
||||
foreach ($line in $output) {
|
||||
$text = $line.ToString()
|
||||
if ($text -match $pattern) {
|
||||
$versions += $Matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ($versions.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return @($versions | Sort-Object { [version]$_ } -Descending | Select-Object -First 1)[0]
|
||||
}
|
||||
|
||||
function Install-AndroidSdkPackage {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$SdkManagerCommand,
|
||||
[Parameter(Mandatory=$true)][string]$PackageId
|
||||
)
|
||||
|
||||
$licenseInput = ("y`n" * 100)
|
||||
$output = @($licenseInput | & $SdkManagerCommand "--sdk_root=$SdkRoot" "--install" $PackageId 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
throw "sdkmanager failed to install $PackageId with exit code ${exitCode}: $($output -join [Environment]::NewLine)"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-LatestAndroidSdkPackageDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SdkRoot,
|
||||
[Parameter(Mandatory=$true)][string]$PackageName,
|
||||
[string]$SdkManagerCommand
|
||||
)
|
||||
|
||||
$installedBefore = $null
|
||||
try {
|
||||
$installedBefore = Get-LatestAndroidSdkPackageDirectory -SdkRoot $SdkRoot -PackageName $PackageName
|
||||
} catch {
|
||||
$installedBefore = $null
|
||||
}
|
||||
|
||||
$availableVersion = $null
|
||||
$action = "using-installed"
|
||||
if ($SdkManagerCommand) {
|
||||
$availableVersion = Get-LatestAvailableAndroidSdkPackageVersion `
|
||||
-SdkRoot $SdkRoot `
|
||||
-SdkManagerCommand $SdkManagerCommand `
|
||||
-PackageName $PackageName
|
||||
|
||||
$installedVersion = if ($installedBefore) { [version]$installedBefore.Name } else { $null }
|
||||
$availableParsed = if ($availableVersion) { [version]$availableVersion } else { $null }
|
||||
if ($availableParsed -and (!$installedVersion -or $availableParsed -gt $installedVersion)) {
|
||||
Install-AndroidSdkPackage `
|
||||
-SdkRoot $SdkRoot `
|
||||
-SdkManagerCommand $SdkManagerCommand `
|
||||
-PackageId "$PackageName;$availableVersion"
|
||||
$action = "installed-latest-available"
|
||||
} elseif ($availableParsed) {
|
||||
$action = "already-latest-available"
|
||||
} else {
|
||||
$action = "available-version-not-listed"
|
||||
}
|
||||
} elseif (!$installedBefore) {
|
||||
throw "No installed Android SDK package versions found under $(Join-Path $SdkRoot $PackageName), and sdkmanager was not found."
|
||||
}
|
||||
|
||||
$selected = Get-LatestAndroidSdkPackageDirectory -SdkRoot $SdkRoot -PackageName $PackageName
|
||||
return [ordered]@{
|
||||
directory = $selected
|
||||
update = [ordered]@{
|
||||
package = $PackageName
|
||||
installedVersionBefore = if ($installedBefore) { $installedBefore.Name } else { $null }
|
||||
availableVersion = $availableVersion
|
||||
selectedVersion = $selected.Name
|
||||
action = $action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Set-AndroidSdkToolchainEnvironment {
|
||||
$sdkRoot = Get-AndroidSdkRoot
|
||||
$sdkManagerCommand = Get-AndroidSdkManagerCommand -SdkRoot $sdkRoot
|
||||
$ndkSelection = Ensure-LatestAndroidSdkPackageDirectory `
|
||||
-SdkRoot $sdkRoot `
|
||||
-PackageName "ndk" `
|
||||
-SdkManagerCommand $sdkManagerCommand
|
||||
$cmakeSelection = Ensure-LatestAndroidSdkPackageDirectory `
|
||||
-SdkRoot $sdkRoot `
|
||||
-PackageName "cmake" `
|
||||
-SdkManagerCommand $sdkManagerCommand
|
||||
|
||||
$ndk = $ndkSelection.directory
|
||||
$cmake = $cmakeSelection.directory
|
||||
$cmakeCommand = Join-Path $cmake.FullName "bin\cmake.exe"
|
||||
|
||||
if (!(Test-Path -LiteralPath $cmakeCommand -PathType Leaf)) {
|
||||
throw "Android SDK CMake executable not found: $cmakeCommand"
|
||||
}
|
||||
|
||||
$env:ANDROID_HOME = $sdkRoot
|
||||
$env:ANDROID_SDK_ROOT = $sdkRoot
|
||||
$env:ANDROID_NDK_HOME = $ndk.FullName
|
||||
$env:ANDROID_NDK_ROOT = $ndk.FullName
|
||||
|
||||
return [ordered]@{
|
||||
sdkRoot = $sdkRoot
|
||||
sdkManagerCommand = $sdkManagerCommand
|
||||
packageUpdates = @($ndkSelection.update, $cmakeSelection.update)
|
||||
ndkVersion = $ndk.Name
|
||||
ndkPath = $ndk.FullName
|
||||
cmakeVersion = $cmake.Name
|
||||
cmakeCommand = $cmakeCommand
|
||||
}
|
||||
}
|
||||
201
scripts/automation/android-sdk-env.sh
Normal file
201
scripts/automation/android-sdk-env.sh
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
android_sdk_root() {
|
||||
if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT" ]; then
|
||||
printf '%s\n' "$ANDROID_SDK_ROOT"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${ANDROID_HOME:-}" ] && [ -d "$ANDROID_HOME" ]; then
|
||||
printf '%s\n' "$ANDROID_HOME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${LOCALAPPDATA:-}" ]; then
|
||||
local_sdk="$LOCALAPPDATA/Android/Sdk"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
local_sdk="$(cygpath -u "$local_sdk")"
|
||||
fi
|
||||
if [ -d "$local_sdk" ]; then
|
||||
printf '%s\n' "$local_sdk"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d "$HOME/Android/Sdk" ]; then
|
||||
printf '%s\n' "$HOME/Android/Sdk"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
latest_android_package_dir() {
|
||||
root="$1"
|
||||
package="$2"
|
||||
package_root="$root/$package"
|
||||
[ -d "$package_root" ] || return 1
|
||||
latest="$(
|
||||
for dir in "$package_root"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
version="${dir##*/}"
|
||||
printf '%s\n' "$version"
|
||||
done | grep -E '^[0-9]+(\.[0-9]+)*$' | sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -n 1
|
||||
)"
|
||||
[ -n "$latest" ] || return 1
|
||||
printf '%s/%s\n' "$package_root" "$latest"
|
||||
}
|
||||
|
||||
android_sdkmanager_command() {
|
||||
root="$1"
|
||||
for candidate in \
|
||||
"$root/cmdline-tools/latest/bin/sdkmanager" \
|
||||
"$root/cmdline-tools/latest/bin/sdkmanager.bat" \
|
||||
"$root/tools/bin/sdkmanager" \
|
||||
"$root/tools/bin/sdkmanager.bat"
|
||||
do
|
||||
if [ -x "$candidate" ] || [ -f "$candidate" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -d "$root/cmdline-tools" ]; then
|
||||
for version in "$root/cmdline-tools"/*; do
|
||||
[ -d "$version" ] || continue
|
||||
[ "${version##*/}" = "latest" ] && continue
|
||||
for candidate in "$version/bin/sdkmanager" "$version/bin/sdkmanager.bat"; do
|
||||
if [ -x "$candidate" ] || [ -f "$candidate" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v sdkmanager >/dev/null 2>&1; then
|
||||
command -v sdkmanager
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
latest_version_from_stdin() {
|
||||
sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n -k 5,5n | tail -n 1
|
||||
}
|
||||
|
||||
latest_available_android_package_version() {
|
||||
root="$1"
|
||||
sdkmanager_cmd="$2"
|
||||
package="$3"
|
||||
output="$("$sdkmanager_cmd" "--sdk_root=$root" --list)" || return 1
|
||||
printf '%s\n' "$output" |
|
||||
sed -n "s/^[[:space:]]*$package;\\([0-9][0-9.]*\\)[[:space:]]*|.*/\\1/p" |
|
||||
latest_version_from_stdin
|
||||
}
|
||||
|
||||
accept_android_sdk_licenses() {
|
||||
i=0
|
||||
while [ "$i" -lt 100 ]; do
|
||||
printf '%s\n' "y"
|
||||
i="$((i + 1))"
|
||||
done
|
||||
}
|
||||
|
||||
install_android_sdk_package() {
|
||||
root="$1"
|
||||
sdkmanager_cmd="$2"
|
||||
package_id="$3"
|
||||
accept_android_sdk_licenses | "$sdkmanager_cmd" "--sdk_root=$root" --install "$package_id" >&2
|
||||
}
|
||||
|
||||
record_android_package_update() {
|
||||
package="$1"
|
||||
installed_before="$2"
|
||||
available="$3"
|
||||
selected="$4"
|
||||
action="$5"
|
||||
|
||||
case "$package" in
|
||||
ndk)
|
||||
ANDROID_NDK_INSTALLED_BEFORE="$installed_before"
|
||||
ANDROID_NDK_AVAILABLE_VERSION="$available"
|
||||
ANDROID_NDK_UPDATE_ACTION="$action"
|
||||
;;
|
||||
cmake)
|
||||
ANDROID_CMAKE_INSTALLED_BEFORE="$installed_before"
|
||||
ANDROID_CMAKE_AVAILABLE_VERSION="$available"
|
||||
ANDROID_CMAKE_UPDATE_ACTION="$action"
|
||||
;;
|
||||
esac
|
||||
export ANDROID_NDK_INSTALLED_BEFORE ANDROID_NDK_AVAILABLE_VERSION ANDROID_NDK_UPDATE_ACTION
|
||||
export ANDROID_CMAKE_INSTALLED_BEFORE ANDROID_CMAKE_AVAILABLE_VERSION ANDROID_CMAKE_UPDATE_ACTION
|
||||
printf '%s\n' "$selected" >/dev/null
|
||||
}
|
||||
|
||||
ensure_latest_android_package_dir() {
|
||||
root="$1"
|
||||
package="$2"
|
||||
sdkmanager_cmd="${3:-}"
|
||||
|
||||
installed_dir="$(latest_android_package_dir "$root" "$package" 2>/dev/null || true)"
|
||||
installed_before="${installed_dir##*/}"
|
||||
[ -n "$installed_dir" ] || installed_before=""
|
||||
available_version=""
|
||||
action="using-installed"
|
||||
|
||||
if [ -n "$sdkmanager_cmd" ]; then
|
||||
available_version="$(latest_available_android_package_version "$root" "$sdkmanager_cmd" "$package")" || return 1
|
||||
if [ -n "$available_version" ]; then
|
||||
if [ -z "$installed_before" ]; then
|
||||
install_android_sdk_package "$root" "$sdkmanager_cmd" "$package;$available_version" || return 1
|
||||
action="installed-latest-available"
|
||||
else
|
||||
newest="$(printf '%s\n%s\n' "$installed_before" "$available_version" | latest_version_from_stdin)"
|
||||
if [ "$newest" = "$available_version" ] && [ "$available_version" != "$installed_before" ]; then
|
||||
install_android_sdk_package "$root" "$sdkmanager_cmd" "$package;$available_version" || return 1
|
||||
action="installed-latest-available"
|
||||
else
|
||||
action="already-latest-available"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
action="available-version-not-listed"
|
||||
fi
|
||||
elif [ -z "$installed_dir" ]; then
|
||||
printf '%s\n' "No installed Android SDK package was found under $root/$package, and sdkmanager was not found." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
selected_dir="$(latest_android_package_dir "$root" "$package")" || return 1
|
||||
record_android_package_update "$package" "$installed_before" "$available_version" "${selected_dir##*/}" "$action"
|
||||
printf '%s\n' "$selected_dir"
|
||||
}
|
||||
|
||||
set_android_sdk_toolchain_environment() {
|
||||
sdk_root="$(android_sdk_root)" || {
|
||||
printf '%s\n' "Android SDK root was not found. Install command-line tools or set ANDROID_SDK_ROOT." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
sdkmanager_cmd="$(android_sdkmanager_command "$sdk_root" || true)"
|
||||
|
||||
ndk_dir="$(ensure_latest_android_package_dir "$sdk_root" ndk "$sdkmanager_cmd")" || return 1
|
||||
cmake_dir="$(ensure_latest_android_package_dir "$sdk_root" cmake "$sdkmanager_cmd")" || return 1
|
||||
|
||||
cmake_command="$cmake_dir/bin/cmake"
|
||||
[ -x "$cmake_command" ] || {
|
||||
printf '%s\n' "Android SDK CMake executable not found: $cmake_command" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
export ANDROID_HOME="$sdk_root"
|
||||
export ANDROID_SDK_ROOT="$sdk_root"
|
||||
export ANDROID_NDK_HOME="$ndk_dir"
|
||||
export ANDROID_NDK_ROOT="$ndk_dir"
|
||||
export ANDROID_CMAKE_COMMAND="$cmake_command"
|
||||
export ANDROID_NDK_VERSION="${ndk_dir##*/}"
|
||||
export ANDROID_CMAKE_VERSION="${cmake_dir##*/}"
|
||||
export ANDROID_SDKMANAGER_COMMAND="$sdkmanager_cmd"
|
||||
}
|
||||
93
scripts/automation/apple-remote-build.ps1
Normal file
93
scripts/automation/apple-remote-build.ps1
Normal file
@@ -0,0 +1,93 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$HostName = "panopainter-mac",
|
||||
[string]$RemoteDirectory = "~/Dev/panopainter",
|
||||
[string]$RepositoryUrl = "ssh://git@git.omar.synology.me:3022/omar/panopainter.git",
|
||||
[string]$Branch = "codex/modernization-cmake-foundation",
|
||||
[string[]]$Presets = @("macos", "ios-simulator", "ios-device")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Join-RemoteArgument {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return ($expanded -join " ")
|
||||
}
|
||||
|
||||
function ConvertTo-ShellSingleQuoted {
|
||||
param([string]$Value)
|
||||
|
||||
return "'" + ($Value -replace "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
$presetArgument = Join-RemoteArgument -Values $Presets
|
||||
$remoteDirectoryLiteral = ConvertTo-ShellSingleQuoted -Value $RemoteDirectory
|
||||
$repositoryLiteral = ConvertTo-ShellSingleQuoted -Value $RepositoryUrl
|
||||
$branchLiteral = ConvertTo-ShellSingleQuoted -Value $Branch
|
||||
$presetLiteral = ConvertTo-ShellSingleQuoted -Value $presetArgument
|
||||
|
||||
$remoteScript = @"
|
||||
set -eu
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:`$HOME/tools/bin:`$PATH"
|
||||
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
|
||||
remote_dir=$remoteDirectoryLiteral
|
||||
repository_url=$repositoryLiteral
|
||||
branch_name=$branchLiteral
|
||||
presets=$presetLiteral
|
||||
|
||||
case "`$remote_dir" in
|
||||
"~/"*) remote_dir="`$HOME/`$(printf '%s' "`$remote_dir" | sed 's|^~/||')" ;;
|
||||
esac
|
||||
|
||||
mkdir -p "`$(dirname "`$remote_dir")"
|
||||
if [ ! -d "`$remote_dir/.git" ]; then
|
||||
git clone "`$repository_url" "`$remote_dir"
|
||||
fi
|
||||
|
||||
cd "`$remote_dir"
|
||||
git fetch origin
|
||||
git checkout "`$branch_name"
|
||||
git pull --ff-only origin "`$branch_name"
|
||||
|
||||
git submodule update --init --recursive \
|
||||
libs/tinyxml2 \
|
||||
libs/glm \
|
||||
libs/stb/stb \
|
||||
libs/yoga \
|
||||
libs/poly2tri \
|
||||
libs/base64 \
|
||||
libs/sqlite3 \
|
||||
libs/nanort \
|
||||
libs/hash-library \
|
||||
libs/fmt \
|
||||
libs/glad \
|
||||
libs/tinyfiledialogs
|
||||
|
||||
mkdir -p out/logs
|
||||
log="out/logs/apple-platform-build-`$(date +%Y%m%d-%H%M%S).log"
|
||||
set +e
|
||||
sh ./scripts/automation/platform-build.sh "`$presets" > "`$log" 2>&1
|
||||
exit_code=`$?
|
||||
set -e
|
||||
|
||||
printf '{"command":"apple-remote-build","host":"%s","branch":"%s","presets":"%s","log":"%s","exitCode":%s}\n' \
|
||||
"`$(hostname)" "`$branch_name" "`$presets" "`$log" "`$exit_code"
|
||||
tail -n 80 "`$log"
|
||||
exit "`$exit_code"
|
||||
"@
|
||||
|
||||
$remoteScript = $remoteScript -replace "`r`n", "`n"
|
||||
$encodedRemoteScript = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($remoteScript))
|
||||
& ssh -o BatchMode=yes $HostName "printf '%s' '$encodedRemoteScript' | base64 -D | sh"
|
||||
exit $LASTEXITCODE
|
||||
62
scripts/automation/check-renderer-boundary.ps1
Normal file
62
scripts/automation/check-renderer-boundary.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Root = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($Root)) {
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
}
|
||||
$started = Get-Date
|
||||
$pattern = '\b(?:GL|WGL)_[A-Z0-9_]+\b'
|
||||
$allowed = @(
|
||||
"src/renderer_gl/",
|
||||
"src/rtt.cpp",
|
||||
"src/texture.cpp"
|
||||
)
|
||||
$violations = @()
|
||||
$files = Get-ChildItem -Path (Join-Path $Root "src") -Recurse -File -Include *.c,*.cc,*.cpp,*.h,*.hpp
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/').Replace('\', '/')
|
||||
$isAllowed = $false
|
||||
foreach ($prefix in $allowed) {
|
||||
if ($relative.StartsWith($prefix)) {
|
||||
$isAllowed = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($isAllowed) {
|
||||
continue
|
||||
}
|
||||
|
||||
$lineNumber = 0
|
||||
foreach ($line in Get-Content -LiteralPath $file.FullName) {
|
||||
$lineNumber += 1
|
||||
$trimmed = $line.TrimStart()
|
||||
if ($trimmed.StartsWith("//")) {
|
||||
continue
|
||||
}
|
||||
$matches = [regex]::Matches($line, $pattern)
|
||||
foreach ($match in $matches) {
|
||||
$violations += [ordered]@{
|
||||
file = $relative
|
||||
line = $lineNumber
|
||||
token = $match.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$exitCode = if ($violations.Count -eq 0) { 0 } else { 1 }
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
command = "check-renderer-boundary"
|
||||
exitCode = $exitCode
|
||||
violationCount = $violations.Count
|
||||
violations = @($violations | Select-Object -First 50)
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress -Depth 4
|
||||
|
||||
exit $exitCode
|
||||
34
scripts/automation/check-renderer-boundary.sh
Normal file
34
scripts/automation/check-renderer-boundary.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
root="${1:-$(CDPATH= cd -- "$script_dir/../.." && pwd)}"
|
||||
start="$(date +%s)"
|
||||
tmp="${TMPDIR:-/tmp}/panopainter-renderer-boundary-$$.txt"
|
||||
|
||||
find "$root/src" -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) | while IFS= read -r file; do
|
||||
rel="${file#"$root"/}"
|
||||
case "$rel" in
|
||||
src/renderer_gl/*|src/rtt.cpp|src/texture.cpp)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
awk -v rel="$rel" '
|
||||
/^[[:space:]]*\/\// { next }
|
||||
match($0, /\<(GL|WGL)_[A-Z0-9_]+\>/) {
|
||||
print rel ":" FNR ":" substr($0, RSTART, RLENGTH)
|
||||
}
|
||||
' "$file"
|
||||
done > "$tmp"
|
||||
|
||||
count="$(wc -l < "$tmp" | tr -d '[:space:]')"
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
exit_code="0"
|
||||
if [ "$count" -ne 0 ]; then
|
||||
exit_code="1"
|
||||
fi
|
||||
|
||||
printf '{"command":"check-renderer-boundary","exitCode":%s,"violationCount":%s,"elapsedMs":%s}\n' "$exit_code" "$count" "$elapsed_ms"
|
||||
rm -f "$tmp"
|
||||
exit "$exit_code"
|
||||
@@ -2,13 +2,361 @@
|
||||
param(
|
||||
[string]$Preset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Target = "PanoPainter"
|
||||
[string]$Target = "PanoPainter",
|
||||
[string]$CMakeCommand = "cmake",
|
||||
[switch]$ReadinessOnly,
|
||||
[switch]$AndroidNativeChecks,
|
||||
[string[]]$PackageKinds = @(
|
||||
"windows-appx",
|
||||
"android-standard-apk",
|
||||
"android-quest-apk",
|
||||
"android-focus-apk",
|
||||
"apple-bundle",
|
||||
"linux-app",
|
||||
"webgl"
|
||||
)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$started = Get-Date
|
||||
$root = (Get-Location).Path
|
||||
|
||||
& cmake --build --preset $Preset --config $Configuration --target $Target
|
||||
function Test-CommandAvailable {
|
||||
param([string]$Name)
|
||||
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
function New-ArtifactCheck {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Path,
|
||||
[string]$PathType = "Any"
|
||||
)
|
||||
|
||||
$exists = if ($PathType -eq "Container") {
|
||||
Test-Path -LiteralPath $Path -PathType Container
|
||||
} elseif ($PathType -eq "Leaf") {
|
||||
Test-Path -LiteralPath $Path -PathType Leaf
|
||||
} else {
|
||||
Test-Path -LiteralPath $Path
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
name = $Name
|
||||
path = $Path
|
||||
pathType = $PathType
|
||||
exists = $exists
|
||||
}
|
||||
}
|
||||
|
||||
function New-Prerequisite {
|
||||
param(
|
||||
[string]$Name,
|
||||
[bool]$Available,
|
||||
[string]$Detail
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
name = $Name
|
||||
available = $Available
|
||||
detail = $Detail
|
||||
}
|
||||
}
|
||||
|
||||
function New-PackageReadiness {
|
||||
param(
|
||||
[string]$Kind,
|
||||
[string]$Status,
|
||||
[string]$Reason,
|
||||
[object[]]$Prerequisites,
|
||||
[object[]]$Artifacts,
|
||||
[string]$ValidationCommand
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
kind = $Kind
|
||||
status = $Status
|
||||
reason = $Reason
|
||||
debt = "DEBT-0011"
|
||||
validationCommand = $ValidationCommand
|
||||
prerequisites = $Prerequisites
|
||||
artifacts = $Artifacts
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AndroidNativeCheckPlan {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$packages = @()
|
||||
if ($Kinds -contains "android-standard-apk") {
|
||||
$packages += [ordered]@{
|
||||
packages = @("standard")
|
||||
configureOnly = $false
|
||||
}
|
||||
}
|
||||
|
||||
$configureOnlyPackages = @()
|
||||
if ($Kinds -contains "android-quest-apk") {
|
||||
$configureOnlyPackages += "quest"
|
||||
}
|
||||
if ($Kinds -contains "android-focus-apk") {
|
||||
$configureOnlyPackages += "focus"
|
||||
}
|
||||
if ($configureOnlyPackages.Count -gt 0) {
|
||||
$packages += [ordered]@{
|
||||
packages = $configureOnlyPackages
|
||||
configureOnly = $true
|
||||
}
|
||||
}
|
||||
|
||||
return $packages
|
||||
}
|
||||
|
||||
function Invoke-AndroidNativePackageChecks {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$plans = @(Get-AndroidNativeCheckPlan -Kinds $Kinds)
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($plan in $plans) {
|
||||
$arguments = @(
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Join-Path $root "scripts/automation/android-legacy-package-build.ps1"),
|
||||
"-Packages", ($plan.packages -join ",")
|
||||
)
|
||||
if ($plan.configureOnly) {
|
||||
$arguments += "-ConfigureOnly"
|
||||
}
|
||||
|
||||
$output = @(& powershell @arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $exitCode
|
||||
}
|
||||
|
||||
$jsonLine = @($output | ForEach-Object { $_.ToString() } | Where-Object { $_.TrimStart().StartsWith("{") } | Select-Object -Last 1)
|
||||
$summary = $null
|
||||
if ($jsonLine.Count -gt 0) {
|
||||
try {
|
||||
$summary = $jsonLine[-1] | ConvertFrom-Json
|
||||
} catch {
|
||||
$summary = $null
|
||||
}
|
||||
}
|
||||
|
||||
$results += [ordered]@{
|
||||
packages = $plan.packages
|
||||
configureOnly = [bool]$plan.configureOnly
|
||||
exitCode = $exitCode
|
||||
command = "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages $($plan.packages -join ',')$(if ($plan.configureOnly) { ' -ConfigureOnly' } else { '' })"
|
||||
summary = $summary
|
||||
}
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
requested = $plans.Count -gt 0
|
||||
exitCode = $overallExitCode
|
||||
results = $results
|
||||
}
|
||||
}
|
||||
|
||||
$PackageKinds = @(Expand-ArgumentList -Values $PackageKinds)
|
||||
|
||||
function Get-PackageReadiness {
|
||||
param([string[]]$Kinds)
|
||||
|
||||
$readiness = @()
|
||||
foreach ($kind in $Kinds) {
|
||||
switch ($kind) {
|
||||
"windows-appx" {
|
||||
$wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||
$manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest"
|
||||
$appPackages = Join-Path $root "PanoPainterPackage/AppPackages"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
|
||||
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
|
||||
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
|
||||
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-standard-apk" {
|
||||
$gradle = Join-Path $root "android/android/build.gradle"
|
||||
$manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/android/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/android assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-quest-apk" {
|
||||
$gradle = Join-Path $root "android/quest/build.gradle"
|
||||
$manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/quest/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/quest assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"android-focus-apk" {
|
||||
$gradle = Join-Path $root "android/focus/build.gradle"
|
||||
$manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml"
|
||||
$apkDir = Join-Path $root "android/focus/build/outputs/apk"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "gradle -p android/focus assembleDebug" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
|
||||
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
|
||||
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
|
||||
(New-Prerequisite -Name "retained-native-cmake-check" -Available $true -Detail "powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"apple-bundle" {
|
||||
$xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj"
|
||||
$bundleDir = Join-Path $root "out/package/apple"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
|
||||
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
|
||||
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
"linux-app" {
|
||||
$linuxCmake = Join-Path $root "linux/CMakeLists.txt"
|
||||
$linuxBinary = Join-Path $root "out/package/linux/panopainter"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "retained-linux-cmake-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "retained-linux-cmake" -Available (Test-Path -LiteralPath $linuxCmake -PathType Leaf) -Detail $linuxCmake),
|
||||
(New-Prerequisite -Name "cmake" -Available (Test-CommandAvailable "cmake") -Detail "Linux retained app CMake configure/build tool"),
|
||||
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "linux-clang"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "linux-app-output" -Path $linuxBinary -PathType "Leaf")
|
||||
)
|
||||
}
|
||||
"webgl" {
|
||||
$webglCmake = Join-Path $root "webgl/CMakeLists.txt"
|
||||
$webDir = Join-Path $root "out/package/webgl"
|
||||
$readiness += New-PackageReadiness `
|
||||
-Kind $kind `
|
||||
-Status "blocked" `
|
||||
-Reason "retained-webgl-cmake-not-consuming-root-cmake-targets" `
|
||||
-ValidationCommand "emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter" `
|
||||
-Prerequisites @(
|
||||
(New-Prerequisite -Name "retained-webgl-cmake" -Available (Test-Path -LiteralPath $webglCmake -PathType Leaf) -Detail $webglCmake),
|
||||
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
|
||||
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
|
||||
(New-Prerequisite -Name "retained-platform-cmake-baseline" -Available $true -Detail "python scripts/dev/check_retained_platform_cmake.py"),
|
||||
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
|
||||
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
|
||||
) `
|
||||
-Artifacts @(
|
||||
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $readiness
|
||||
}
|
||||
|
||||
$androidNativeValidation = if ($AndroidNativeChecks) {
|
||||
Invoke-AndroidNativePackageChecks -Kinds $PackageKinds
|
||||
} else {
|
||||
[ordered]@{
|
||||
requested = $false
|
||||
exitCode = 0
|
||||
results = @()
|
||||
}
|
||||
}
|
||||
|
||||
if ($ReadinessOnly) {
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
[ordered]@{
|
||||
command = "package-smoke"
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "readiness"
|
||||
exitCode = 0
|
||||
elapsedMs = $elapsed
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
exit $androidNativeValidation.exitCode
|
||||
}
|
||||
|
||||
& $CMakeCommand --build --preset $Preset --config $Configuration --target $Target
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0) {
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
@@ -18,21 +366,35 @@ if ($buildExitCode -ne 0) {
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
stage = "build"
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $buildExitCode
|
||||
elapsedMs = $elapsed
|
||||
} | ConvertTo-Json -Compress
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
exit $buildExitCode
|
||||
}
|
||||
|
||||
$binaryDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "$Target.exe"
|
||||
$dataDir = Join-Path (Join-Path (Join-Path (Get-Location) "out/build/$Preset") $Configuration) "data"
|
||||
$targetDir = Split-Path -Parent $binaryDir
|
||||
$dataDir = Join-Path $targetDir "data"
|
||||
$curlDll = if ($Configuration -eq "Debug") { "libcurl_debug.dll" } else { "libcurl.dll" }
|
||||
$checks = @(
|
||||
[ordered]@{ name = "executable"; path = $binaryDir; exists = Test-Path -LiteralPath $binaryDir -PathType Leaf },
|
||||
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container }
|
||||
[ordered]@{ name = "data"; path = $dataDir; exists = Test-Path -LiteralPath $dataDir -PathType Container },
|
||||
[ordered]@{ name = "BugTrapU-x64.dll"; path = (Join-Path $targetDir "BugTrapU-x64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "BugTrapU-x64.dll") -PathType Leaf },
|
||||
[ordered]@{ name = $curlDll; path = (Join-Path $targetDir $curlDll); exists = Test-Path -LiteralPath (Join-Path $targetDir $curlDll) -PathType Leaf },
|
||||
[ordered]@{ name = "libyuv.dll"; path = (Join-Path $targetDir "libyuv.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libyuv.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "libmp4v2.dll"; path = (Join-Path $targetDir "libmp4v2.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "libmp4v2.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "openh264-2.0.0-win64.dll"; path = (Join-Path $targetDir "openh264-2.0.0-win64.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openh264-2.0.0-win64.dll") -PathType Leaf },
|
||||
[ordered]@{ name = "openvr_api.dll"; path = (Join-Path $targetDir "openvr_api.dll"); exists = Test-Path -LiteralPath (Join-Path $targetDir "openvr_api.dll") -PathType Leaf }
|
||||
)
|
||||
|
||||
$failed = @($checks | Where-Object { -not $_.exists })
|
||||
$exitCode = if ($failed.Count -eq 0) { 0 } else { 2 }
|
||||
if ($androidNativeValidation.exitCode -ne 0 -and $exitCode -eq 0) {
|
||||
$exitCode = $androidNativeValidation.exitCode
|
||||
}
|
||||
$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
|
||||
[ordered]@{
|
||||
@@ -40,9 +402,12 @@ $elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
preset = $Preset
|
||||
configuration = $Configuration
|
||||
target = $Target
|
||||
cmakeCommand = $CMakeCommand
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsedMs
|
||||
checks = $checks
|
||||
} | ConvertTo-Json -Compress -Depth 5
|
||||
androidNativeValidation = $androidNativeValidation
|
||||
packageReadiness = @(Get-PackageReadiness -Kinds $PackageKinds)
|
||||
} | ConvertTo-Json -Compress -Depth 8
|
||||
|
||||
exit $exitCode
|
||||
|
||||
@@ -5,14 +5,117 @@ preset="${1:-linux-clang}"
|
||||
configuration="${2:-Debug}"
|
||||
target="${3:-PanoPainter}"
|
||||
artifact="${4:-out/build/$preset/$target}"
|
||||
readiness_only=0
|
||||
if [ "${1:-}" = "--readiness-only" ]; then
|
||||
readiness_only=1
|
||||
preset="${2:-linux-clang}"
|
||||
configuration="${3:-Debug}"
|
||||
target="${4:-PanoPainter}"
|
||||
artifact="${5:-out/build/$preset/$target}"
|
||||
fi
|
||||
start="$(date +%s)"
|
||||
root="$(pwd)"
|
||||
|
||||
json_string() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
}
|
||||
|
||||
json_bool() {
|
||||
if [ "$1" = "1" ]; then
|
||||
printf true
|
||||
else
|
||||
printf false
|
||||
fi
|
||||
}
|
||||
|
||||
command_available() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
file_available() {
|
||||
[ -f "$1" ]
|
||||
}
|
||||
|
||||
dir_available() {
|
||||
[ -d "$1" ]
|
||||
}
|
||||
|
||||
package_readiness_json() {
|
||||
windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj"
|
||||
windows_manifest="$root/PanoPainterPackage/Package.appxmanifest"
|
||||
windows_output="$root/PanoPainterPackage/AppPackages"
|
||||
android_standard_gradle="$root/android/android/build.gradle"
|
||||
android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml"
|
||||
android_standard_output="$root/android/android/build/outputs/apk"
|
||||
android_quest_gradle="$root/android/quest/build.gradle"
|
||||
android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml"
|
||||
android_quest_output="$root/android/quest/build/outputs/apk"
|
||||
android_focus_gradle="$root/android/focus/build.gradle"
|
||||
android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml"
|
||||
android_focus_output="$root/android/focus/build/outputs/apk"
|
||||
apple_project="$root/PanoPainter.xcodeproj/project.pbxproj"
|
||||
apple_output="$root/out/package/apple"
|
||||
linux_cmake="$root/linux/CMakeLists.txt"
|
||||
linux_output="$root/out/package/linux/panopainter"
|
||||
webgl_cmake="$root/webgl/CMakeLists.txt"
|
||||
webgl_output="$root/out/package/webgl"
|
||||
|
||||
file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$linux_cmake"; linux_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available cmake; cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
file_available "$linux_output"; linux_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
file_available "$webgl_cmake"; webgl_cmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
|
||||
|
||||
printf '['
|
||||
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
|
||||
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages standard"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
|
||||
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages quest -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
|
||||
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"retained-native-cmake-check","available":true,"detail":"powershell -ExecutionPolicy Bypass -File scripts/automation/android-legacy-package-build.ps1 -Packages focus -ConfigureOnly"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
|
||||
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
|
||||
printf ',{"kind":"linux-app","status":"blocked","reason":"retained-linux-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"cmake -S linux -B out/package/linux-retained && cmake --build out/package/linux-retained --target panopainter","prerequisites":[{"name":"retained-linux-cmake","available":%s,"detail":%s},{"name":"cmake","available":%s,"detail":"Linux retained app CMake configure/build tool"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"linux-clang"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"linux-app-output","path":%s,"pathType":"Leaf","exists":%s}]}' "$(json_bool "$linux_cmake_exists")" "$(json_string "$linux_cmake")" "$(json_bool "$cmake_exists")" "$(json_string "$linux_output")" "$(json_bool "$linux_output_exists")"
|
||||
printf ',{"kind":"webgl","status":"blocked","reason":"retained-webgl-cmake-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"emcmake cmake -S webgl -B out/package/webgl-retained && cmake --build out/package/webgl-retained --target panopainter","prerequisites":[{"name":"retained-webgl-cmake","available":%s,"detail":%s},{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"retained-platform-cmake-baseline","available":true,"detail":"python scripts/dev/check_retained_platform_cmake.py"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$webgl_cmake_exists")" "$(json_string "$webgl_cmake")" "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
|
||||
printf ']'
|
||||
}
|
||||
|
||||
if [ "$readiness_only" -eq 1 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
readiness="$(package_readiness_json)"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"readiness","exitCode":0,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$elapsed_ms" "$readiness"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmake --build --preset "$preset" --config "$configuration" --target "$target"
|
||||
build_exit="$?"
|
||||
if [ "$build_exit" -ne 0 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms"
|
||||
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
|
||||
|
||||
@@ -24,5 +127,6 @@ fi
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms"
|
||||
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"
|
||||
|
||||
@@ -1,16 +1,162 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64"),
|
||||
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_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_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
|
||||
[string[]]$Presets = @("android-arm64", "android-x64", "android-quest-arm64", "android-focus-arm64"),
|
||||
[string[]]$Targets = @(
|
||||
"pp_foundation",
|
||||
"pp_assets",
|
||||
"pp_paint",
|
||||
"pp_document",
|
||||
"pp_renderer_api",
|
||||
"pp_renderer_gl",
|
||||
"pp_paint_renderer",
|
||||
"pp_ui_core",
|
||||
"pp_platform_api",
|
||||
"pp_app_core",
|
||||
"pano_cli",
|
||||
"pp_foundation_binary_stream_tests",
|
||||
"pp_foundation_event_tests",
|
||||
"pp_foundation_log_tests",
|
||||
"pp_foundation_parse_tests",
|
||||
"pp_foundation_task_queue_tests",
|
||||
"pp_foundation_trace_tests",
|
||||
"pp_assets_brush_package_tests",
|
||||
"pp_assets_image_format_tests",
|
||||
"pp_assets_image_metadata_tests",
|
||||
"pp_assets_image_pixels_tests",
|
||||
"pp_assets_ppi_header_tests",
|
||||
"pp_assets_settings_document_tests",
|
||||
"pp_paint_brush_tests",
|
||||
"pp_paint_blend_tests",
|
||||
"pp_paint_stroke_tests",
|
||||
"pp_paint_stroke_script_tests",
|
||||
"pp_document_tests",
|
||||
"pp_document_ppi_import_tests",
|
||||
"pp_document_ppi_export_tests",
|
||||
"pp_renderer_api_tests",
|
||||
"pp_renderer_gl_capabilities_tests",
|
||||
"pp_renderer_gl_command_plan_tests",
|
||||
"pp_paint_renderer_compositor_tests",
|
||||
"pp_platform_api_tests",
|
||||
"pp_ui_core_color_tests",
|
||||
"pp_ui_core_layout_value_tests",
|
||||
"pp_ui_core_layout_xml_tests",
|
||||
"pp_ui_core_node_lifetime_tests",
|
||||
"pp_ui_core_overlay_lifetime_tests",
|
||||
"pp_app_core_about_menu_tests",
|
||||
"pp_app_core_app_dialog_tests",
|
||||
"pp_app_core_app_preferences_tests",
|
||||
"pp_app_core_app_frame_tests",
|
||||
"pp_app_core_app_thread_tests",
|
||||
"pp_app_core_app_input_tests",
|
||||
"pp_app_core_app_shutdown_tests",
|
||||
"pp_app_core_app_startup_tests",
|
||||
"pp_app_core_app_status_tests",
|
||||
"pp_app_core_command_convert_tests",
|
||||
"pp_app_core_brush_package_export_tests",
|
||||
"pp_app_core_brush_package_import_tests",
|
||||
"pp_app_core_brush_ui_tests",
|
||||
"pp_app_core_canvas_hotkey_tests",
|
||||
"pp_app_core_canvas_tool_ui_tests",
|
||||
"pp_app_core_canvas_view_tests",
|
||||
"pp_app_core_document_animation_tests",
|
||||
"pp_app_core_document_canvas_tests",
|
||||
"pp_app_core_document_cloud_tests",
|
||||
"pp_app_core_document_export_tests",
|
||||
"pp_app_core_document_import_tests",
|
||||
"pp_app_core_document_layer_tests",
|
||||
"pp_app_core_document_platform_io_tests",
|
||||
"pp_app_core_document_recording_tests",
|
||||
"pp_app_core_document_resize_tests",
|
||||
"pp_app_core_document_route_tests",
|
||||
"pp_app_core_document_sharing_tests",
|
||||
"pp_app_core_document_session_tests",
|
||||
"pp_app_core_file_menu_tests",
|
||||
"pp_app_core_grid_ui_tests",
|
||||
"pp_app_core_history_ui_tests",
|
||||
"pp_app_core_main_toolbar_tests",
|
||||
"pp_app_core_quick_ui_tests",
|
||||
"pp_app_core_tools_menu_tests"
|
||||
)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
foreach ($part in ($value -split ",")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed.Length -gt 0) {
|
||||
$expanded += $trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded
|
||||
}
|
||||
|
||||
function Get-VcpkgRoot {
|
||||
$candidates = @()
|
||||
if ($env:VCPKG_ROOT) {
|
||||
$candidates += $env:VCPKG_ROOT
|
||||
}
|
||||
|
||||
$programFiles = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ }
|
||||
$vsYears = @("2026", "2022")
|
||||
$vsEditions = @("Community", "Professional", "Enterprise", "BuildTools", "Preview")
|
||||
foreach ($root in $programFiles) {
|
||||
foreach ($year in $vsYears) {
|
||||
foreach ($edition in $vsEditions) {
|
||||
$candidates += (Join-Path $root "Microsoft Visual Studio\$year\$edition\VC\vcpkg")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath (Join-Path $candidate "vcpkg.exe") -PathType Leaf)) {
|
||||
return (Resolve-Path -LiteralPath $candidate).Path
|
||||
}
|
||||
}
|
||||
|
||||
throw "VCPKG_ROOT was not set and no Visual Studio bundled vcpkg root was found."
|
||||
}
|
||||
|
||||
function Set-VcpkgRootEnvironment {
|
||||
$vcpkgRoot = Get-VcpkgRoot
|
||||
$env:VCPKG_ROOT = $vcpkgRoot
|
||||
return [ordered]@{
|
||||
vcpkgRoot = $vcpkgRoot
|
||||
vcpkgCommand = Join-Path $vcpkgRoot "vcpkg.exe"
|
||||
}
|
||||
}
|
||||
|
||||
$Presets = @(Expand-ArgumentList -Values $Presets)
|
||||
$Targets = @(Expand-ArgumentList -Values $Targets)
|
||||
|
||||
$cmakeCommand = "cmake"
|
||||
$androidToolchain = $null
|
||||
$vcpkgToolchain = $null
|
||||
if ($Presets | Where-Object { $_ -like "*vcpkg*" }) {
|
||||
$vcpkgToolchain = Set-VcpkgRootEnvironment
|
||||
}
|
||||
if ($Presets | Where-Object { $_ -like "android-*" }) {
|
||||
. "$PSScriptRoot\android-sdk-env.ps1"
|
||||
$androidToolchain = Set-AndroidSdkToolchainEnvironment
|
||||
$cmakeCommand = $androidToolchain.cmakeCommand
|
||||
}
|
||||
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
foreach ($preset in $Presets) {
|
||||
& cmake --preset $preset
|
||||
$presetCmakeCommand = $cmakeCommand
|
||||
if ($androidToolchain -and $preset -notlike "android-*") {
|
||||
$presetCmakeCommand = "cmake"
|
||||
}
|
||||
|
||||
& $presetCmakeCommand --preset $preset
|
||||
$configureExitCode = $LASTEXITCODE
|
||||
if ($configureExitCode -ne 0) {
|
||||
$overallExitCode = $configureExitCode
|
||||
@@ -27,7 +173,7 @@ foreach ($preset in $Presets) {
|
||||
$buildArgs += @("--target", $target)
|
||||
}
|
||||
|
||||
& cmake @buildArgs
|
||||
& $presetCmakeCommand @buildArgs
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
if ($buildExitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $buildExitCode
|
||||
@@ -46,6 +192,8 @@ $elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
command = "platform-build"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
androidToolchain = $androidToolchain
|
||||
vcpkgToolchain = $vcpkgToolchain
|
||||
results = $results
|
||||
} | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
#!/usr/bin/env sh
|
||||
set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_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_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests pp_app_core_about_menu_tests pp_app_core_app_dialog_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}"
|
||||
start="$(date +%s)"
|
||||
|
||||
cmake --preset "$preset"
|
||||
configure_exit="$?"
|
||||
if [ "$configure_exit" -ne 0 ]; then
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","stage":"configure","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configure_exit" "$elapsed_ms"
|
||||
exit "$configure_exit"
|
||||
fi
|
||||
android_cmake_cmd=""
|
||||
case " $presets " in
|
||||
*" android-"*)
|
||||
# shellcheck disable=SC1091
|
||||
. "$(dirname "$0")/android-sdk-env.sh"
|
||||
set_android_sdk_toolchain_environment || exit 1
|
||||
android_cmake_cmd="$ANDROID_CMAKE_COMMAND"
|
||||
;;
|
||||
esac
|
||||
|
||||
overall_exit=0
|
||||
results=""
|
||||
first_result=1
|
||||
|
||||
build_args=""
|
||||
for target in $targets; do
|
||||
build_args="$build_args --target $target"
|
||||
done
|
||||
|
||||
normalized_presets="$(printf '%s' "$presets" | tr ',' ' ')"
|
||||
for preset in $normalized_presets; do
|
||||
cmake_cmd="cmake"
|
||||
case "$preset" in
|
||||
android-*)
|
||||
cmake_cmd="$android_cmake_cmd"
|
||||
;;
|
||||
esac
|
||||
|
||||
"$cmake_cmd" --preset "$preset"
|
||||
configure_exit="$?"
|
||||
if [ "$configure_exit" -ne 0 ]; then
|
||||
[ "$overall_exit" -eq 0 ] && overall_exit="$configure_exit"
|
||||
result="$(printf '{"preset":"%s","stage":"configure","exitCode":%s}' "$preset" "$configure_exit")"
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
cmake --build --preset "$preset" $build_args
|
||||
"$cmake_cmd" --build --preset "$preset" $build_args
|
||||
build_exit="$?"
|
||||
[ "$build_exit" -ne 0 ] && [ "$overall_exit" -eq 0 ] && overall_exit="$build_exit"
|
||||
result="$(printf '{"preset":"%s","stage":"build","targets":"%s","exitCode":%s}' "$preset" "$targets" "$build_exit")"
|
||||
fi
|
||||
|
||||
if [ "$first_result" -eq 1 ]; then
|
||||
results="$result"
|
||||
first_result=0
|
||||
else
|
||||
results="$results,$result"
|
||||
fi
|
||||
done
|
||||
|
||||
end="$(date +%s)"
|
||||
elapsed_ms="$(( (end - start) * 1000 ))"
|
||||
printf '{"command":"platform-build","preset":"%s","targets":"%s","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$targets" "$build_exit" "$elapsed_ms"
|
||||
exit "$build_exit"
|
||||
if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -n "${ANDROID_CMAKE_COMMAND:-}" ]; then
|
||||
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"androidToolchain":{"sdkRoot":"%s","sdkManagerCommand":"%s","packageUpdates":[{"package":"ndk","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"},{"package":"cmake","installedVersionBefore":"%s","availableVersion":"%s","selectedVersion":"%s","action":"%s"}],"ndkVersion":"%s","ndkPath":"%s","cmakeVersion":"%s","cmakeCommand":"%s"},"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$ANDROID_SDK_ROOT" "$ANDROID_SDKMANAGER_COMMAND" "$ANDROID_NDK_INSTALLED_BEFORE" "$ANDROID_NDK_AVAILABLE_VERSION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_UPDATE_ACTION" "$ANDROID_CMAKE_INSTALLED_BEFORE" "$ANDROID_CMAKE_AVAILABLE_VERSION" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_UPDATE_ACTION" "$ANDROID_NDK_VERSION" "$ANDROID_NDK_HOME" "$ANDROID_CMAKE_VERSION" "$ANDROID_CMAKE_COMMAND" "$results"
|
||||
else
|
||||
printf '{"command":"platform-build","exitCode":%s,"elapsedMs":%s,"results":[%s]}\n' "$overall_exit" "$elapsed_ms" "$results"
|
||||
fi
|
||||
exit "$overall_exit"
|
||||
|
||||
304
scripts/automation/quiet-validate.ps1
Normal file
304
scripts/automation/quiet-validate.ps1
Normal file
@@ -0,0 +1,304 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BuildPreset = "windows-msvc-default",
|
||||
[string]$Configuration = "Debug",
|
||||
[string[]]$BuildTargets = @("PanoPainter", "pano_cli"),
|
||||
[string]$TestPreset = "desktop-fast",
|
||||
[string]$TestRegex = "",
|
||||
[switch]$Configure,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipTests,
|
||||
[string]$CMakeCommand = "",
|
||||
[string]$CTestCommand = "",
|
||||
[string]$LogDir = "out/logs/quiet-validation",
|
||||
[string]$IgnoreFilterFile = "",
|
||||
[string[]]$IgnorePattern = @(),
|
||||
[int]$FailureTailLines = 0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-CMakeCommand {
|
||||
param([string]$Requested)
|
||||
|
||||
if ($Requested.Length -gt 0) {
|
||||
return $Requested
|
||||
}
|
||||
|
||||
$vsCmake = "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
||||
if (Test-Path -LiteralPath $vsCmake) {
|
||||
return $vsCmake
|
||||
}
|
||||
|
||||
return "cmake"
|
||||
}
|
||||
|
||||
function Resolve-CTestCommand {
|
||||
param(
|
||||
[string]$Requested,
|
||||
[string]$ResolvedCMake
|
||||
)
|
||||
|
||||
if ($Requested.Length -gt 0) {
|
||||
return $Requested
|
||||
}
|
||||
|
||||
if ($ResolvedCMake.EndsWith("cmake.exe", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$candidate = Join-Path -Path (Split-Path -Parent $ResolvedCMake) -ChildPath "ctest.exe"
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return "ctest"
|
||||
}
|
||||
|
||||
function Read-IgnorePatterns {
|
||||
param(
|
||||
[string]$FilterFile,
|
||||
[string[]]$InlinePatterns
|
||||
)
|
||||
|
||||
$patterns = @()
|
||||
if ($FilterFile.Length -eq 0) {
|
||||
$defaultFile = Join-Path -Path $PSScriptRoot -ChildPath "quiet-validation-ignore.txt"
|
||||
if (Test-Path -LiteralPath $defaultFile) {
|
||||
$FilterFile = $defaultFile
|
||||
}
|
||||
}
|
||||
|
||||
if ($FilterFile.Length -gt 0 -and (Test-Path -LiteralPath $FilterFile)) {
|
||||
$patterns += Get-Content -LiteralPath $FilterFile |
|
||||
Where-Object { $_.Trim().Length -gt 0 -and -not $_.TrimStart().StartsWith("#") }
|
||||
}
|
||||
|
||||
$patterns += $InlinePatterns
|
||||
return @($patterns | Where-Object { $_ -and $_.Length -gt 0 })
|
||||
}
|
||||
|
||||
function Expand-ArgumentList {
|
||||
param([string[]]$Values)
|
||||
|
||||
$expanded = @()
|
||||
foreach ($value in $Values) {
|
||||
if ($null -eq $value) {
|
||||
continue
|
||||
}
|
||||
$expanded += $value -split "[,\s]+" | Where-Object { $_.Length -gt 0 }
|
||||
}
|
||||
return @($expanded)
|
||||
}
|
||||
|
||||
function Limit-LogSlug {
|
||||
param(
|
||||
[string]$Value,
|
||||
[int]$MaxLength = 96
|
||||
)
|
||||
|
||||
if ($Value.Length -le $MaxLength) {
|
||||
return $Value
|
||||
}
|
||||
return $Value.Substring(0, $MaxLength)
|
||||
}
|
||||
|
||||
function Test-IgnoredLine {
|
||||
param(
|
||||
[string]$Line,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
if ($Line -match $pattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Measure-Log {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string[]]$IgnorePatterns
|
||||
)
|
||||
|
||||
$errorPattern = "(?i)(:\s*(fatal\s+)?error\s+[A-Z0-9]+:|^LINK\s*:\s*fatal error|^CMake Error|Errors while running CTest|Unable to find executable|\*\*\*Failed)"
|
||||
$warningPattern = "(?i)(:\s*warning\s+[A-Z0-9]+:|^LINK\s*:\s*warning\s+[A-Z0-9]+:|warning:)"
|
||||
$ctestSummaryPattern = "(\d+)% tests passed, (\d+) tests failed out of (\d+)"
|
||||
|
||||
$lineCount = 0
|
||||
$rawErrors = 0
|
||||
$rawWarnings = 0
|
||||
$visibleErrors = 0
|
||||
$visibleWarnings = 0
|
||||
$ignoredErrors = 0
|
||||
$ignoredWarnings = 0
|
||||
$testsFailed = $null
|
||||
$testsTotal = $null
|
||||
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
++$lineCount
|
||||
$ignored = Test-IgnoredLine -Line $line -Patterns $IgnorePatterns
|
||||
if ($line -match $ctestSummaryPattern) {
|
||||
$testsFailed = [int]$Matches[2]
|
||||
$testsTotal = [int]$Matches[3]
|
||||
}
|
||||
if ($line -match $errorPattern) {
|
||||
++$rawErrors
|
||||
if ($ignored) { ++$ignoredErrors } else { ++$visibleErrors }
|
||||
}
|
||||
if ($line -match $warningPattern) {
|
||||
++$rawWarnings
|
||||
if ($ignored) { ++$ignoredWarnings } else { ++$visibleWarnings }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [ordered]@{
|
||||
lineCount = $lineCount
|
||||
errors = $visibleErrors
|
||||
warnings = $visibleWarnings
|
||||
rawErrors = $rawErrors
|
||||
rawWarnings = $rawWarnings
|
||||
ignoredErrors = $ignoredErrors
|
||||
ignoredWarnings = $ignoredWarnings
|
||||
testsFailed = $testsFailed
|
||||
testsTotal = $testsTotal
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-QuietStep {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Command,
|
||||
[string[]]$Arguments,
|
||||
[string]$LogPath,
|
||||
[string[]]$IgnorePatterns,
|
||||
[int]$FailureTailLines
|
||||
)
|
||||
|
||||
$started = Get-Date
|
||||
$exitCode = 0
|
||||
try {
|
||||
& $Command @Arguments *> $LogPath
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($null -eq $exitCode) {
|
||||
$exitCode = 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$_ | Out-File -LiteralPath $LogPath -Append -Encoding utf8
|
||||
$exitCode = 1
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
$summary = Measure-Log -Path $LogPath -IgnorePatterns $IgnorePatterns
|
||||
$result = [ordered]@{
|
||||
name = $Name
|
||||
exitCode = $exitCode
|
||||
elapsedMs = $elapsed
|
||||
log = $LogPath
|
||||
summary = $summary
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0 -and $FailureTailLines -gt 0 -and (Test-Path -LiteralPath $LogPath)) {
|
||||
$result.failureTail = @(Get-Content -LiteralPath $LogPath -Tail $FailureTailLines | ForEach-Object { [string]$_ })
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
$resolvedCMake = Resolve-CMakeCommand -Requested $CMakeCommand
|
||||
$resolvedCTest = Resolve-CTestCommand -Requested $CTestCommand -ResolvedCMake $resolvedCMake
|
||||
$BuildTargets = @(Expand-ArgumentList -Values $BuildTargets)
|
||||
$IgnorePattern = @(Expand-ArgumentList -Values $IgnorePattern)
|
||||
$ignorePatterns = Read-IgnorePatterns -FilterFile $IgnoreFilterFile -InlinePatterns $IgnorePattern
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$runId = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$started = Get-Date
|
||||
$results = @()
|
||||
$overallExitCode = 0
|
||||
|
||||
if ($Configure) {
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-configure-$BuildPreset.log"
|
||||
$result = Invoke-QuietStep `
|
||||
-Name "configure:$BuildPreset" `
|
||||
-Command $resolvedCMake `
|
||||
-Arguments @("--preset", $BuildPreset) `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
$targets = @($BuildTargets | Where-Object { $_ -and $_.Length -gt 0 })
|
||||
if ($targets.Count -gt 0) {
|
||||
$safeTargets = Limit-LogSlug -Value (($targets -join "_") -replace "[^A-Za-z0-9_.-]", "_")
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-build-$BuildPreset-$Configuration-$safeTargets.log"
|
||||
$buildArgs = @("--build", "--preset", $BuildPreset, "--config", $Configuration, "--target") + $targets
|
||||
$result = Invoke-QuietStep `
|
||||
-Name ("build:{0}:{1}" -f $BuildPreset, $Configuration) `
|
||||
-Command $resolvedCMake `
|
||||
-Arguments $buildArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
$result.targets = $targets
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipTests) {
|
||||
$safeRegex = if ($TestRegex.Length -gt 0) {
|
||||
Limit-LogSlug -Value ($TestRegex -replace "[^A-Za-z0-9_.-]", "_")
|
||||
} else {
|
||||
"all"
|
||||
}
|
||||
$log = Join-Path -Path $LogDir -ChildPath "$runId-test-$TestPreset-$Configuration-$safeRegex.log"
|
||||
$testArgs = @("--preset", $TestPreset, "--build-config", $Configuration, "--output-on-failure")
|
||||
if ($TestRegex.Length -gt 0) {
|
||||
$testArgs += @("-R", $TestRegex)
|
||||
}
|
||||
$result = Invoke-QuietStep `
|
||||
-Name ("test:{0}:{1}" -f $TestPreset, $Configuration) `
|
||||
-Command $resolvedCTest `
|
||||
-Arguments $testArgs `
|
||||
-LogPath $log `
|
||||
-IgnorePatterns $ignorePatterns `
|
||||
-FailureTailLines $FailureTailLines
|
||||
if ($TestRegex.Length -gt 0) {
|
||||
$result.testRegex = $TestRegex
|
||||
}
|
||||
$results += $result
|
||||
if ($result.exitCode -ne 0 -and $overallExitCode -eq 0) {
|
||||
$overallExitCode = $result.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
|
||||
$summaryPath = Join-Path -Path $LogDir -ChildPath "$runId-summary.json"
|
||||
$payload = [ordered]@{
|
||||
command = "quiet-validate"
|
||||
exitCode = $overallExitCode
|
||||
elapsedMs = $elapsed
|
||||
buildPreset = $BuildPreset
|
||||
configuration = $Configuration
|
||||
testPreset = $TestPreset
|
||||
logDir = $LogDir
|
||||
summary = $summaryPath
|
||||
ignoreFilterFile = $IgnoreFilterFile
|
||||
ignorePatternCount = $ignorePatterns.Count
|
||||
results = $results
|
||||
}
|
||||
|
||||
$payload | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $summaryPath -Encoding utf8
|
||||
$payload | ConvertTo-Json -Compress -Depth 8
|
||||
exit $overallExitCode
|
||||
13
scripts/automation/quiet-validation-ignore.txt
Normal file
13
scripts/automation/quiet-validation-ignore.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Regex patterns for warnings/noise hidden from quiet validation summaries.
|
||||
# The full logs still contain these lines; this file only affects visible counts.
|
||||
The vcpkg manifest was disabled
|
||||
warning C4201:
|
||||
warning C4267:
|
||||
warning C5311:
|
||||
warning C4018:
|
||||
warning C4244:
|
||||
warning C4189:
|
||||
warning C4305:
|
||||
warning C4099:
|
||||
warning LNK4099: PDB 'yuv\.pdb'
|
||||
warning LNK4098: defaultlib 'MSVCRT' conflicts
|
||||
144
scripts/dev/check_package_smoke_readiness.py
Normal file
144
scripts/dev/check_package_smoke_readiness.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify package-smoke wrappers report the expected package readiness matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXPECTED_PACKAGE_KINDS = [
|
||||
"windows-appx",
|
||||
"android-standard-apk",
|
||||
"android-quest-apk",
|
||||
"android-focus-apk",
|
||||
"apple-bundle",
|
||||
"linux-app",
|
||||
"webgl",
|
||||
]
|
||||
|
||||
EXPECTED_CMAKE_PACKAGE_TARGETS = [
|
||||
"panopainter_package_readiness",
|
||||
"panopainter_windows_app_package_smoke",
|
||||
"panopainter_android_standard_native_package",
|
||||
"panopainter_android_vr_native_package_configure",
|
||||
"panopainter_android_native_package_smoke",
|
||||
"panopainter_linux_webgl_package_readiness",
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def powershell_package_kinds(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8")
|
||||
match = re.search(r"\[string\[\]\]\$PackageKinds\s*=\s*@\((.*?)\n\s*\)", script, re.S)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find PackageKinds default in package-smoke.ps1")
|
||||
return sorted(re.findall(r'"([^"]+)"', match.group(1)))
|
||||
|
||||
|
||||
def shell_package_kinds(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8")
|
||||
return sorted(set(re.findall(r'"kind":"([^"]+)"', script)))
|
||||
|
||||
|
||||
def count_regex(root: Path, patterns: dict[str, str]) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for script_name, pattern in patterns.items():
|
||||
text = (root / "scripts" / "automation" / script_name).read_text(encoding="utf-8")
|
||||
counts[script_name] = len(re.findall(pattern, text))
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
expected = sorted(EXPECTED_PACKAGE_KINDS)
|
||||
ps_kinds = powershell_package_kinds(root)
|
||||
sh_kinds = shell_package_kinds(root)
|
||||
debt_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r'debt\s*=\s*"DEBT-0011"',
|
||||
"package-smoke.sh": r'"debt":"DEBT-0011"',
|
||||
})
|
||||
blocked_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r'-Status\s+"blocked"',
|
||||
"package-smoke.sh": r'"status":"blocked"',
|
||||
})
|
||||
readiness_mode_counts = {
|
||||
"package-smoke.ps1": (root / "scripts" / "automation" / "package-smoke.ps1").read_text(encoding="utf-8").count("ReadinessOnly"),
|
||||
"package-smoke.sh": (root / "scripts" / "automation" / "package-smoke.sh").read_text(encoding="utf-8").count("readiness_only"),
|
||||
}
|
||||
retained_android_native_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r"retained-native-cmake-check",
|
||||
"package-smoke.sh": r"retained-native-cmake-check",
|
||||
})
|
||||
retained_platform_cmake_counts = count_regex(root, {
|
||||
"package-smoke.ps1": r"retained-(linux|webgl)-cmake",
|
||||
"package-smoke.sh": r"retained-(linux|webgl)-cmake",
|
||||
})
|
||||
cmake_package_module = (root / "cmake" / "PanoPainterPackageTargets.cmake").read_text(encoding="utf-8")
|
||||
root_cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
|
||||
missing = {
|
||||
"package-smoke.ps1": [kind for kind in expected if kind not in ps_kinds],
|
||||
"package-smoke.sh": [kind for kind in expected if kind not in sh_kinds],
|
||||
}
|
||||
unexpected = {
|
||||
"package-smoke.ps1": [kind for kind in ps_kinds if kind not in expected],
|
||||
"package-smoke.sh": [kind for kind in sh_kinds if kind not in expected],
|
||||
}
|
||||
debt_thresholds = {
|
||||
"package-smoke.ps1": 1,
|
||||
"package-smoke.sh": len(expected),
|
||||
}
|
||||
debt_complete = {name: count >= debt_thresholds[name] for name, count in debt_counts.items()}
|
||||
blocked_complete = {name: count >= len(expected) for name, count in blocked_counts.items()}
|
||||
readiness_mode_present = {name: count > 0 for name, count in readiness_mode_counts.items()}
|
||||
retained_android_native_complete = {
|
||||
name: count >= 3 for name, count in retained_android_native_counts.items()
|
||||
}
|
||||
retained_platform_cmake_complete = {
|
||||
name: count >= 2 for name, count in retained_platform_cmake_counts.items()
|
||||
}
|
||||
cmake_package_targets_present = {
|
||||
target: target in cmake_package_module for target in EXPECTED_CMAKE_PACKAGE_TARGETS
|
||||
}
|
||||
cmake_package_module_included = "include(PanoPainterPackageTargets)" in root_cmake
|
||||
|
||||
ok = (
|
||||
all(not values for values in missing.values())
|
||||
and all(not values for values in unexpected.values())
|
||||
and all(debt_complete.values())
|
||||
and all(blocked_complete.values())
|
||||
and all(readiness_mode_present.values())
|
||||
and all(retained_android_native_complete.values())
|
||||
and all(retained_platform_cmake_complete.values())
|
||||
and all(cmake_package_targets_present.values())
|
||||
and cmake_package_module_included
|
||||
)
|
||||
|
||||
print(json.dumps({
|
||||
"ok": ok,
|
||||
"expectedPackageKinds": expected,
|
||||
"packageKinds": {
|
||||
"package-smoke.ps1": ps_kinds,
|
||||
"package-smoke.sh": sh_kinds,
|
||||
},
|
||||
"missing": missing,
|
||||
"unexpected": unexpected,
|
||||
"debtComplete": debt_complete,
|
||||
"blockedComplete": blocked_complete,
|
||||
"readinessModePresent": readiness_mode_present,
|
||||
"retainedAndroidNativeComplete": retained_android_native_complete,
|
||||
"retainedPlatformCmakeComplete": retained_platform_cmake_complete,
|
||||
"cmakePackageTargetsPresent": cmake_package_targets_present,
|
||||
"cmakePackageModuleIncluded": cmake_package_module_included,
|
||||
}, separators=(",", ":")))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
153
scripts/dev/check_platform_build_targets.py
Normal file
153
scripts/dev/check_platform_build_targets.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify platform-build wrappers include the current headless target matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REQUIRED_COMPONENT_TARGETS = [
|
||||
"pp_foundation",
|
||||
"pp_assets",
|
||||
"pp_paint",
|
||||
"pp_document",
|
||||
"pp_renderer_api",
|
||||
"pp_renderer_gl",
|
||||
"pp_paint_renderer",
|
||||
"pp_ui_core",
|
||||
"pp_platform_api",
|
||||
"pp_app_core",
|
||||
"pano_cli",
|
||||
]
|
||||
|
||||
REQUIRED_ANDROID_PRESETS = [
|
||||
"android-arm64",
|
||||
"android-x64",
|
||||
"android-quest-arm64",
|
||||
"android-focus-arm64",
|
||||
]
|
||||
|
||||
EXPECTED_CMAKE_PLATFORM_TARGETS = [
|
||||
"panopainter_platform_build_headless",
|
||||
"panopainter_platform_build_android_assets",
|
||||
"panopainter_platform_build_vcpkg_ui_core",
|
||||
"panopainter_platform_build_apple_remote",
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def cmake_test_targets(root: Path) -> list[str]:
|
||||
cmake_lists = root / "tests" / "CMakeLists.txt"
|
||||
text = cmake_lists.read_text(encoding="utf-8")
|
||||
return sorted(set(re.findall(r"^\s*add_executable\(([^\s\)]+)", text, re.MULTILINE)))
|
||||
|
||||
|
||||
def powershell_default_targets(root: Path) -> list[str]:
|
||||
script = root / "scripts" / "automation" / "platform-build.ps1"
|
||||
targets: list[str] = []
|
||||
in_targets = False
|
||||
|
||||
for line in script.read_text(encoding="utf-8").splitlines():
|
||||
if "[string[]]$Targets" in line:
|
||||
in_targets = True
|
||||
if not in_targets:
|
||||
continue
|
||||
|
||||
targets.extend(re.findall(r'"([^"]+)"', line))
|
||||
if in_targets and line.strip() == ")":
|
||||
break
|
||||
|
||||
return sorted(set(targets))
|
||||
|
||||
|
||||
def powershell_default_presets(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "platform-build.ps1").read_text(encoding="utf-8")
|
||||
match = re.search(r"\[string\[\]\]\$Presets\s*=\s*@\((.*?)\)", script, re.S)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default presets in platform-build.ps1")
|
||||
return sorted(set(re.findall(r'"([^"]+)"', match.group(1))))
|
||||
|
||||
|
||||
def shell_default_targets(root: Path) -> list[str]:
|
||||
script = root / "scripts" / "automation" / "platform-build.sh"
|
||||
text = script.read_text(encoding="utf-8")
|
||||
match = re.search(r'targets="\$\{[^:]+:-(.*)\}"', text)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default targets in platform-build.sh")
|
||||
return sorted(set(match.group(1).split()))
|
||||
|
||||
|
||||
def shell_default_presets(root: Path) -> list[str]:
|
||||
script = (root / "scripts" / "automation" / "platform-build.sh").read_text(encoding="utf-8")
|
||||
match = re.search(r'presets="\$\{[^:]+:-(.*)\}"', script)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find default presets in platform-build.sh")
|
||||
return sorted(set(match.group(1).split()))
|
||||
|
||||
|
||||
def missing(expected: list[str], actual: list[str]) -> list[str]:
|
||||
actual_set = set(actual)
|
||||
return [target for target in expected if target not in actual_set]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
expected = sorted(set(REQUIRED_COMPONENT_TARGETS + cmake_test_targets(root)))
|
||||
ps_targets = powershell_default_targets(root)
|
||||
sh_targets = shell_default_targets(root)
|
||||
ps_presets = powershell_default_presets(root)
|
||||
sh_presets = shell_default_presets(root)
|
||||
cmake_platform_module = (root / "cmake" / "PanoPainterPlatformTargets.cmake").read_text(encoding="utf-8")
|
||||
root_cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
||||
cmake_platform_targets_present = {
|
||||
target: target in cmake_platform_module for target in EXPECTED_CMAKE_PLATFORM_TARGETS
|
||||
}
|
||||
cmake_platform_module_included = "include(PanoPainterPlatformTargets)" in root_cmake
|
||||
android_sdk_env = {
|
||||
"android-sdk-env.ps1": (root / "scripts" / "automation" / "android-sdk-env.ps1").read_text(encoding="utf-8"),
|
||||
"android-sdk-env.sh": (root / "scripts" / "automation" / "android-sdk-env.sh").read_text(encoding="utf-8"),
|
||||
}
|
||||
android_sdkmanager_update_support = {
|
||||
name: all(token in text for token in ("sdkmanager", "--list", "--install", "ndk", "cmake"))
|
||||
for name, text in android_sdk_env.items()
|
||||
}
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"expectedTargetCount": len(expected),
|
||||
"powershellTargetCount": len(ps_targets),
|
||||
"shellTargetCount": len(sh_targets),
|
||||
"expectedAndroidPresets": REQUIRED_ANDROID_PRESETS,
|
||||
"defaultPresets": {
|
||||
"platform-build.ps1": ps_presets,
|
||||
"platform-build.sh": sh_presets,
|
||||
},
|
||||
"cmakePlatformTargetsPresent": cmake_platform_targets_present,
|
||||
"cmakePlatformModuleIncluded": cmake_platform_module_included,
|
||||
"androidSdkmanagerUpdateSupport": android_sdkmanager_update_support,
|
||||
"missing": {
|
||||
"platform-build.ps1.targets": missing(expected, ps_targets),
|
||||
"platform-build.sh.targets": missing(expected, sh_targets),
|
||||
"platform-build.ps1.presets": missing(REQUIRED_ANDROID_PRESETS, ps_presets),
|
||||
"platform-build.sh.presets": missing(REQUIRED_ANDROID_PRESETS, sh_presets),
|
||||
},
|
||||
}
|
||||
result["ok"] = (
|
||||
all(not values for values in result["missing"].values())
|
||||
and all(cmake_platform_targets_present.values())
|
||||
and cmake_platform_module_included
|
||||
and all(android_sdkmanager_update_support.values())
|
||||
)
|
||||
|
||||
print(json.dumps(result, separators=(",", ":")))
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
61
scripts/dev/check_retained_platform_cmake.py
Normal file
61
scripts/dev/check_retained_platform_cmake.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard retained non-root platform CMake files during Phase 6 migration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RETAINED_PLATFORM_CMAKE = [
|
||||
Path("linux/CMakeLists.txt"),
|
||||
Path("webgl/CMakeLists.txt"),
|
||||
]
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def cmake_minimum_version(text: str) -> tuple[int, ...] | None:
|
||||
match = re.search(r"cmake_minimum_required\s*\(\s*VERSION\s+([0-9.]+)", text, re.I)
|
||||
if not match:
|
||||
return None
|
||||
return tuple(int(part) for part in match.group(1).split("."))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
results: dict[str, object] = {}
|
||||
ok = True
|
||||
|
||||
for path in RETAINED_PLATFORM_CMAKE:
|
||||
text = (root / path).read_text(encoding="utf-8")
|
||||
minimum_version = cmake_minimum_version(text)
|
||||
has_target_cxx23 = "target_compile_features(panopainter PRIVATE cxx_std_23)" in text
|
||||
has_cxx_extensions_off = "CXX_EXTENSIONS OFF" in text
|
||||
uses_global_cxx_standard_flag = bool(re.search(r"-std=c\+\+14|-std=c\+\+17|-std=c\+\+20", text))
|
||||
platform_ok = (
|
||||
minimum_version is not None
|
||||
and minimum_version >= (3, 10)
|
||||
and has_target_cxx23
|
||||
and has_cxx_extensions_off
|
||||
and not uses_global_cxx_standard_flag
|
||||
)
|
||||
ok = ok and platform_ok
|
||||
results[str(path)] = {
|
||||
"ok": platform_ok,
|
||||
"minimumVersion": ".".join(str(part) for part in minimum_version) if minimum_version else None,
|
||||
"hasTargetCxx23": has_target_cxx23,
|
||||
"hasCxxExtensionsOff": has_cxx_extensions_off,
|
||||
"usesGlobalCxxStandardFlag": uses_global_cxx_standard_flag,
|
||||
}
|
||||
|
||||
print(json.dumps({"ok": ok, "platforms": results}, separators=(",", ":")))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
608
scripts/dev/clangd_nav.py
Normal file
608
scripts/dev/clangd_nav.py
Normal file
@@ -0,0 +1,608 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small clangd navigation helper for agent-friendly C++ code lookup.
|
||||
|
||||
Examples:
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 410 --column 30
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 192 --column 43 --path-regex "src[\\\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Pattern
|
||||
|
||||
|
||||
DEFAULT_BUILD_DIRS = (
|
||||
"out/build/windows-clangcl-asan",
|
||||
"out/build/android-arm64",
|
||||
)
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _find_compile_commands_dir(repo_root: Path, requested: str | None) -> Path:
|
||||
if requested:
|
||||
path = Path(requested).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = repo_root / path
|
||||
if path.is_file():
|
||||
path = path.parent
|
||||
if not (path / "compile_commands.json").exists():
|
||||
raise SystemExit(f"compile_commands.json not found in {path}")
|
||||
return path.resolve()
|
||||
|
||||
env_dir = os.environ.get("PP_CLANGD_COMPILE_COMMANDS_DIR")
|
||||
if env_dir:
|
||||
return _find_compile_commands_dir(repo_root, env_dir)
|
||||
|
||||
for candidate in DEFAULT_BUILD_DIRS:
|
||||
path = repo_root / candidate
|
||||
if (path / "compile_commands.json").exists():
|
||||
return path.resolve()
|
||||
|
||||
matches = sorted((repo_root / "out" / "build").glob("*/compile_commands.json"))
|
||||
if matches:
|
||||
return matches[0].parent.resolve()
|
||||
|
||||
raise SystemExit(
|
||||
"No compile_commands.json found. Configure a Ninja CMake preset first, "
|
||||
"or pass --compile-commands-dir."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_file(repo_root: Path, file_arg: str) -> Path:
|
||||
path = Path(file_arg).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = repo_root / path
|
||||
if not path.exists():
|
||||
raise SystemExit(f"file not found: {path}")
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _read_lsp_message(stream: Any) -> dict[str, Any] | None:
|
||||
content_length: int | None = None
|
||||
while True:
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
return None
|
||||
if line in (b"\r\n", b"\n"):
|
||||
break
|
||||
name, _, value = line.decode("ascii", errors="replace").partition(":")
|
||||
if name.lower() == "content-length":
|
||||
content_length = int(value.strip())
|
||||
|
||||
if content_length is None:
|
||||
return None
|
||||
|
||||
payload = stream.read(content_length)
|
||||
if not payload:
|
||||
return None
|
||||
return json.loads(payload.decode("utf-8"))
|
||||
|
||||
|
||||
def _write_lsp_message(stream: Any, message: dict[str, Any]) -> None:
|
||||
payload = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
||||
header = f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii")
|
||||
stream.write(header + payload)
|
||||
stream.flush()
|
||||
|
||||
|
||||
class ClangdClient:
|
||||
def __init__(
|
||||
self,
|
||||
clangd: str,
|
||||
compile_commands_dir: Path,
|
||||
timeout_seconds: float,
|
||||
background_index: bool) -> None:
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._next_id = 1
|
||||
self._responses: dict[int, dict[str, Any]] = {}
|
||||
self._condition = threading.Condition()
|
||||
self._messages: "queue.Queue[dict[str, Any]]" = queue.Queue()
|
||||
clangd_args = [
|
||||
clangd,
|
||||
f"--compile-commands-dir={compile_commands_dir}",
|
||||
"--log=error",
|
||||
]
|
||||
if not background_index:
|
||||
clangd_args.append("--background-index=false")
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
clangd_args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if self._process.stdin is None or self._process.stdout is None:
|
||||
raise RuntimeError("failed to open clangd stdio pipes")
|
||||
|
||||
self._stdin = self._process.stdin
|
||||
self._stdout = self._process.stdout
|
||||
self._reader = threading.Thread(target=self._reader_loop, daemon=True)
|
||||
self._reader.start()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self.notify("exit", {})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=2)
|
||||
except Exception:
|
||||
self._process.kill()
|
||||
|
||||
def _reader_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
message = _read_lsp_message(self._stdout)
|
||||
except Exception as exc:
|
||||
message = {"error": {"message": f"failed to read clangd response: {exc}"}}
|
||||
if message is None:
|
||||
return
|
||||
if "id" in message:
|
||||
with self._condition:
|
||||
self._responses[int(message["id"])] = message
|
||||
self._condition.notify_all()
|
||||
else:
|
||||
self._messages.put(message)
|
||||
|
||||
def request(self, method: str, params: dict[str, Any]) -> Any:
|
||||
request_id = self._next_id
|
||||
self._next_id += 1
|
||||
_write_lsp_message(
|
||||
self._stdin,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
},
|
||||
)
|
||||
deadline = time.monotonic() + self._timeout_seconds
|
||||
with self._condition:
|
||||
while request_id not in self._responses:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(f"clangd request timed out: {method}")
|
||||
self._condition.wait(remaining)
|
||||
response = self._responses.pop(request_id)
|
||||
|
||||
if "error" in response:
|
||||
raise RuntimeError(response["error"].get("message", "clangd request failed"))
|
||||
return response.get("result")
|
||||
|
||||
def notify(self, method: str, params: dict[str, Any]) -> None:
|
||||
_write_lsp_message(
|
||||
self._stdin,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _position_params(file_path: Path, line: int, column: int) -> dict[str, Any]:
|
||||
if line < 1 or column < 1:
|
||||
raise SystemExit("--line and --column are 1-based and must be positive")
|
||||
return {
|
||||
"textDocument": { "uri": file_path.as_uri() },
|
||||
"position": { "line": line - 1, "character": column - 1 },
|
||||
}
|
||||
|
||||
|
||||
def _range_to_json(range_value: dict[str, Any]) -> dict[str, Any]:
|
||||
start = range_value["start"]
|
||||
end = range_value["end"]
|
||||
return {
|
||||
"start": { "line": start["line"] + 1, "column": start["character"] + 1 },
|
||||
"end": { "line": end["line"] + 1, "column": end["character"] + 1 },
|
||||
}
|
||||
|
||||
|
||||
def _location_to_json(value: dict[str, Any]) -> dict[str, Any]:
|
||||
if "targetUri" in value:
|
||||
uri = value["targetUri"]
|
||||
range_value = value.get("targetRange", value.get("targetSelectionRange"))
|
||||
else:
|
||||
uri = value["uri"]
|
||||
range_value = value["range"]
|
||||
return {
|
||||
"uri": uri,
|
||||
"path": _uri_to_path(uri),
|
||||
"range": _range_to_json(range_value),
|
||||
}
|
||||
|
||||
|
||||
def _uri_to_path(uri: str) -> str:
|
||||
if uri.startswith("file:///"):
|
||||
raw = uri[8:]
|
||||
if len(raw) >= 3 and raw[1] == ":":
|
||||
return raw.replace("/", "\\")
|
||||
return "/" + raw
|
||||
return uri
|
||||
|
||||
|
||||
def _locations_to_json(result: Any) -> list[dict[str, Any]]:
|
||||
if result is None:
|
||||
return []
|
||||
if isinstance(result, dict):
|
||||
return [_location_to_json(result)]
|
||||
return [_location_to_json(item) for item in result]
|
||||
|
||||
|
||||
def _symbols_to_json(symbols: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
def convert(symbol: dict[str, Any]) -> dict[str, Any]:
|
||||
item = {
|
||||
"name": symbol.get("name", ""),
|
||||
"detail": symbol.get("detail", ""),
|
||||
"kind": symbol.get("kind", 0),
|
||||
"range": _range_to_json(symbol["range"]),
|
||||
"selectionRange": _range_to_json(symbol.get("selectionRange", symbol["range"])),
|
||||
}
|
||||
children = symbol.get("children")
|
||||
if children:
|
||||
item["children"] = [convert(child) for child in children]
|
||||
return item
|
||||
|
||||
return [convert(symbol) for symbol in symbols or []]
|
||||
|
||||
|
||||
def _flatten_symbols(symbols: list[dict[str, Any]], parent: str = "") -> list[dict[str, Any]]:
|
||||
flattened: list[dict[str, Any]] = []
|
||||
for symbol in symbols:
|
||||
qualified_name = f"{parent}::{symbol['name']}" if parent else symbol["name"]
|
||||
item = {
|
||||
"name": symbol["name"],
|
||||
"qualifiedName": qualified_name,
|
||||
"detail": symbol.get("detail", ""),
|
||||
"kind": symbol.get("kind", 0),
|
||||
"range": symbol["range"],
|
||||
"selectionRange": symbol["selectionRange"],
|
||||
}
|
||||
flattened.append(item)
|
||||
flattened.extend(_flatten_symbols(symbol.get("children", []), qualified_name))
|
||||
return flattened
|
||||
|
||||
|
||||
def _limit_results(values: list[dict[str, Any]], max_results: int) -> tuple[list[dict[str, Any]], bool]:
|
||||
if max_results < 1:
|
||||
return values, False
|
||||
return values[:max_results], len(values) > max_results
|
||||
|
||||
|
||||
def _hover_to_json(result: Any) -> dict[str, Any] | None:
|
||||
if not result:
|
||||
return None
|
||||
contents = result.get("contents")
|
||||
if isinstance(contents, dict):
|
||||
value = contents.get("value", "")
|
||||
elif isinstance(contents, list):
|
||||
value = "\n".join(str(item.get("value", item)) if isinstance(item, dict) else str(item) for item in contents)
|
||||
else:
|
||||
value = str(contents)
|
||||
output = { "contents": value }
|
||||
if "range" in result:
|
||||
output["range"] = _range_to_json(result["range"])
|
||||
return output
|
||||
|
||||
|
||||
def _compile_optional_regex(pattern: str | None, option_name: str, ignore_case: bool) -> Pattern[str] | None:
|
||||
if not pattern:
|
||||
return None
|
||||
flags = re.IGNORECASE if ignore_case else 0
|
||||
try:
|
||||
return re.compile(pattern, flags)
|
||||
except re.error as exc:
|
||||
raise SystemExit(f"invalid {option_name}: {exc}") from exc
|
||||
|
||||
|
||||
def _regex_matches(regex: Pattern[str] | None, value: str) -> bool:
|
||||
return regex is None or regex.search(value) is not None
|
||||
|
||||
|
||||
def _filter_flat_symbols(
|
||||
symbols: list[dict[str, Any]],
|
||||
name_substring: str | None,
|
||||
name_regex: Pattern[str] | None,
|
||||
detail_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
filtered = symbols
|
||||
if name_substring:
|
||||
needle = name_substring.lower()
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if needle in symbol["qualifiedName"].lower()
|
||||
]
|
||||
if name_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if name_regex.search(symbol["qualifiedName"])
|
||||
]
|
||||
if detail_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if detail_regex.search(symbol.get("detail", ""))
|
||||
]
|
||||
return filtered
|
||||
|
||||
|
||||
def _filter_locations(
|
||||
locations: list[dict[str, Any]],
|
||||
path_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not path_regex:
|
||||
return locations
|
||||
return [
|
||||
location for location in locations
|
||||
if _regex_matches(path_regex, location.get("path", ""))
|
||||
or _regex_matches(path_regex, location.get("uri", ""))
|
||||
]
|
||||
|
||||
|
||||
def _run_self_test() -> int:
|
||||
name_regex = _compile_optional_regex(r"node(panel|dialog)::open_.*", "--name-regex", True)
|
||||
detail_regex = _compile_optional_regex(r"export.*plan", "--detail-regex", True)
|
||||
path_regex = _compile_optional_regex(r"src[\\/]app(_dialogs)?\.cpp$", "--path-regex", True)
|
||||
case_sensitive_regex = _compile_optional_regex(r"Brush", "--name-regex", False)
|
||||
|
||||
symbols = [
|
||||
{
|
||||
"qualifiedName": "NodePanel::open_project",
|
||||
"detail": "void()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "NodeDialog::open_export",
|
||||
"detail": "ExportTargetPlan()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "Brush::open_project",
|
||||
"detail": "BrushPlan()",
|
||||
},
|
||||
]
|
||||
name_matches = _filter_flat_symbols(symbols, None, name_regex, None)
|
||||
detail_matches = _filter_flat_symbols(symbols, None, None, detail_regex)
|
||||
case_sensitive_matches = _filter_flat_symbols(symbols, None, case_sensitive_regex, None)
|
||||
|
||||
locations = [
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app_dialogs.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app_dialogs.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\docs\modernization\roadmap.md",
|
||||
"uri": "file:///D:/Dev/panopainter/docs/modernization/roadmap.md",
|
||||
},
|
||||
]
|
||||
path_matches = _filter_locations(locations, path_regex)
|
||||
|
||||
checks = {
|
||||
"nameRegexMatchesAlternationAndWildcard": len(name_matches) == 2,
|
||||
"detailRegexMatchesSymbolDetail": len(detail_matches) == 1
|
||||
and detail_matches[0]["qualifiedName"] == "NodeDialog::open_export",
|
||||
"caseSensitiveRegexHonorsNoIgnoreCase": len(case_sensitive_matches) == 1
|
||||
and case_sensitive_matches[0]["qualifiedName"] == "Brush::open_project",
|
||||
"pathRegexFiltersLocations": len(path_matches) == 2
|
||||
and all("src" in location["path"] for location in path_matches),
|
||||
}
|
||||
ok = all(checks.values())
|
||||
print(json.dumps(
|
||||
{
|
||||
"ok": ok,
|
||||
"command": "self-test",
|
||||
"checks": checks,
|
||||
},
|
||||
indent=2,
|
||||
))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def _open_document(client: ClangdClient, file_path: Path) -> None:
|
||||
language_id = "cpp"
|
||||
if file_path.suffix.lower() in { ".h", ".hpp", ".hh", ".hxx" }:
|
||||
language_id = "cpp"
|
||||
elif file_path.suffix.lower() == ".c":
|
||||
language_id = "c"
|
||||
|
||||
client.notify(
|
||||
"textDocument/didOpen",
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": file_path.as_uri(),
|
||||
"languageId": language_id,
|
||||
"version": 1,
|
||||
"text": file_path.read_text(encoding="utf-8", errors="replace"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
if args.command == "self-test":
|
||||
return _run_self_test()
|
||||
|
||||
if not args.file:
|
||||
raise SystemExit("--file is required for clangd navigation commands")
|
||||
|
||||
symbol_filters = [args.name, args.name_regex, args.detail_regex]
|
||||
if any(symbol_filters) and args.command != "symbols":
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex are only supported by the symbols command")
|
||||
if args.path_regex and args.command not in { "definition", "declaration", "implementation", "references" }:
|
||||
raise SystemExit("--path-regex is only supported by location commands")
|
||||
if args.hierarchical and any(symbol_filters):
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex require flat symbols; omit --hierarchical")
|
||||
|
||||
repo_root = _repo_root()
|
||||
compile_commands_dir = _find_compile_commands_dir(repo_root, args.compile_commands_dir)
|
||||
file_path = _resolve_file(repo_root, args.file)
|
||||
|
||||
name_regex = _compile_optional_regex(args.name_regex, "--name-regex", args.ignore_case)
|
||||
detail_regex = _compile_optional_regex(args.detail_regex, "--detail-regex", args.ignore_case)
|
||||
path_regex = _compile_optional_regex(args.path_regex, "--path-regex", args.ignore_case)
|
||||
|
||||
if args.command == "references" and not args.background_index and not args.allow_incomplete_references:
|
||||
raise SystemExit(
|
||||
"references may be incomplete without clangd background indexing. "
|
||||
"Pass --background-index for a broader best-effort query or "
|
||||
"--allow-incomplete-references for current-translation-unit lookup."
|
||||
)
|
||||
|
||||
client = ClangdClient(args.clangd, compile_commands_dir, args.timeout, args.background_index)
|
||||
try:
|
||||
client.request(
|
||||
"initialize",
|
||||
{
|
||||
"processId": None,
|
||||
"rootUri": repo_root.as_uri(),
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"definition": { "linkSupport": True },
|
||||
"declaration": { "linkSupport": True },
|
||||
"implementation": { "linkSupport": True },
|
||||
"references": {},
|
||||
"hover": {},
|
||||
"documentSymbol": { "hierarchicalDocumentSymbolSupport": True },
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
client.notify("initialized", {})
|
||||
_open_document(client, file_path)
|
||||
|
||||
command = args.command
|
||||
result: Any
|
||||
result_count: int | None = None
|
||||
truncated = False
|
||||
if command == "symbols":
|
||||
symbols = _symbols_to_json(
|
||||
client.request("textDocument/documentSymbol", { "textDocument": { "uri": file_path.as_uri() } })
|
||||
)
|
||||
if args.hierarchical:
|
||||
result = symbols
|
||||
result_count = len(symbols)
|
||||
else:
|
||||
flattened = _flatten_symbols(symbols)
|
||||
flattened = _filter_flat_symbols(flattened, args.name, name_regex, detail_regex)
|
||||
result_count = len(flattened)
|
||||
result, truncated = _limit_results(flattened, args.max_results)
|
||||
elif command == "hover":
|
||||
result = _hover_to_json(client.request("textDocument/hover", _position_params(file_path, args.line, args.column)))
|
||||
elif command == "references":
|
||||
params = _position_params(file_path, args.line, args.column)
|
||||
params["context"] = { "includeDeclaration": args.include_declaration }
|
||||
locations = _locations_to_json(client.request("textDocument/references", params))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
else:
|
||||
method = {
|
||||
"definition": "textDocument/definition",
|
||||
"declaration": "textDocument/declaration",
|
||||
"implementation": "textDocument/implementation",
|
||||
}[command]
|
||||
locations = _locations_to_json(client.request(method, _position_params(file_path, args.line, args.column)))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
|
||||
print(json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"command": command,
|
||||
"file": str(file_path),
|
||||
"compileCommandsDir": str(compile_commands_dir),
|
||||
"backgroundIndex": args.background_index,
|
||||
"referenceCompleteness": (
|
||||
"not-applicable" if command != "references"
|
||||
else ("best-effort-background-index" if args.background_index else "current-translation-unit-only")
|
||||
),
|
||||
"filters": {
|
||||
"name": args.name,
|
||||
"nameRegex": args.name_regex,
|
||||
"detailRegex": args.detail_regex,
|
||||
"pathRegex": args.path_regex,
|
||||
"ignoreCase": args.ignore_case,
|
||||
},
|
||||
"resultCount": result_count,
|
||||
"truncated": truncated,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
))
|
||||
return 0
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="Navigate C++ symbols through clangd JSON-RPC.")
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=("definition", "declaration", "implementation", "references", "hover", "symbols", "self-test"),
|
||||
)
|
||||
parser.add_argument("--file", help="Source/header file to open.")
|
||||
parser.add_argument("--line", type=int, default=1, help="1-based line for position commands.")
|
||||
parser.add_argument("--column", type=int, default=1, help="1-based column for position commands.")
|
||||
parser.add_argument(
|
||||
"--compile-commands-dir",
|
||||
help="Directory containing compile_commands.json. Defaults to PP_CLANGD_COMPILE_COMMANDS_DIR or known build dirs.",
|
||||
)
|
||||
parser.add_argument("--clangd", default="clangd", help="clangd executable path.")
|
||||
parser.add_argument("--timeout", type=float, default=20.0, help="Request timeout in seconds.")
|
||||
parser.add_argument("--name", help="Case-insensitive symbol-name filter for symbols command.")
|
||||
parser.add_argument("--name-regex", help="Regex filter for symbols command, matched against qualifiedName.")
|
||||
parser.add_argument("--detail-regex", help="Regex filter for symbols command, matched against detail.")
|
||||
parser.add_argument("--path-regex", help="Regex filter for definition/declaration/implementation/references paths.")
|
||||
parser.add_argument(
|
||||
"--ignore-case",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Use case-insensitive regex matching for --name-regex, --detail-regex, and --path-regex. Enabled by default.",
|
||||
)
|
||||
parser.add_argument("--max-results", type=int, default=100, help="Maximum locations/symbols to print; <=0 disables.")
|
||||
parser.add_argument(
|
||||
"--background-index",
|
||||
action="store_true",
|
||||
help="Allow clangd to build/use its background index for broader cross-translation-unit references.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-incomplete-references",
|
||||
action="store_true",
|
||||
help="Permit current-translation-unit-only references when --background-index is not enabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hierarchical",
|
||||
action="store_true",
|
||||
help="Print nested document symbols instead of the compact flat symbol list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-declaration",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Include declaration in references results.",
|
||||
)
|
||||
return run(parser.parse_args(argv))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
59
skills/panopainter-code-navigation/SKILL.md
Normal file
59
skills/panopainter-code-navigation/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: panopainter-code-navigation
|
||||
description: Use when working in the PanoPainter repository and Codex needs to follow C++ symbols, inspect declarations/definitions/hover info, list matching symbols, use regex symbol/detail/path filters, or reduce broad rg searches during refactors. Prefer this before text search for symbol navigation, regex symbol-family lookup, service-interface wiring, override/implementation lookup, or legacy-to-component boundary tracing.
|
||||
---
|
||||
|
||||
# PanoPainter Code Navigation
|
||||
|
||||
Use the repo's clangd helper before broad text searches when a task depends on C++
|
||||
symbol identity rather than plain text. This is required for PanoPainter C++
|
||||
refactors that depend on symbol families, declarations/definitions, override
|
||||
groups, service/interface wiring, or platform/backend boundaries.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the current workspace is `D:\Dev\panopainter`.
|
||||
2. Use this skill before broad text search when a C++ refactor depends on
|
||||
symbol identity, a symbol family, signatures, override groups, or
|
||||
platform/backend boundary paths.
|
||||
3. Prefer reliable lookups:
|
||||
- `symbols`
|
||||
- `definition`
|
||||
- `declaration`
|
||||
- `implementation`
|
||||
- `hover`
|
||||
4. Use regex filters when looking for symbol families, generated-style names, or
|
||||
backend/platform boundary patterns:
|
||||
- `--name-regex` filters flat symbols by `qualifiedName`.
|
||||
- `--detail-regex` filters flat symbols by detail/signature text.
|
||||
- `--path-regex` filters definition/declaration/implementation/reference
|
||||
locations by path or URI.
|
||||
- Regex matching is case-insensitive by default; add `--no-ignore-case` for
|
||||
case-sensitive checks.
|
||||
- Run `python scripts/dev/clangd_nav.py self-test` if the helper changed or
|
||||
regex output looks surprising.
|
||||
5. Use `references` only as advisory:
|
||||
- Pass `--background-index` for broader best-effort references.
|
||||
- Pass `--allow-incomplete-references` only for explicitly current-translation-unit-only references.
|
||||
- Never treat incomplete reference output as proof that no other users exist.
|
||||
6. Keep output small with `--name`, regex filters, and `--max-results`.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py hover --file src/app_core/brush_ui.h --line 783 --column 60
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
The helper uses `PP_CLANGD_COMPILE_COMMANDS_DIR` when set, otherwise it checks
|
||||
known Ninja build trees such as `out/build/windows-clangcl-asan` and
|
||||
`out/build/android-arm64`. Pass `--compile-commands-dir` when using another
|
||||
configured build tree.
|
||||
|
||||
Use normal `rg` for non-symbol text, docs, build files, generated command names,
|
||||
or when clangd cannot parse the relevant file.
|
||||
4
skills/panopainter-code-navigation/agents/openai.yaml
Normal file
4
skills/panopainter-code-navigation/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "PanoPainter Code Navigation"
|
||||
short_description: "Use clangd navigation for PanoPainter C++ symbols."
|
||||
default_prompt: "Use compiler-aware clangd navigation in PanoPainter before broad text searches when following C++ symbols."
|
||||
969
src/app.cpp
969
src/app.cpp
File diff suppressed because it is too large
Load Diff
144
src/app.h
144
src/app.h
@@ -19,30 +19,38 @@
|
||||
#include "node_canvas.h"
|
||||
#include "node_dialog_layer_rename.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "node_panel_quick.h"
|
||||
#include "node_input_box.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "layout.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "app_core/app_thread.h"
|
||||
|
||||
namespace pp::platform {
|
||||
class PlatformServices;
|
||||
struct PlatformStoragePaths;
|
||||
}
|
||||
|
||||
class NodePanelGrid;
|
||||
|
||||
#if defined(__OBJC__) && defined(__IOS__)
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "GameViewController.h"
|
||||
#import "AppDelegate.h"
|
||||
@class GameViewController;
|
||||
@class AppDelegate;
|
||||
#endif
|
||||
|
||||
#if defined(__OBJC__) && defined(__OSX__)
|
||||
#import "main.h"
|
||||
@class View;
|
||||
@class AppOSX;
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "main.h"
|
||||
struct android_app;
|
||||
struct engine;
|
||||
#endif
|
||||
|
||||
#ifdef __LINUX__
|
||||
#include <GLFW/glfw3.h>
|
||||
#if __LINUX__ || __WEB__
|
||||
struct GLFWwindow;
|
||||
#endif
|
||||
|
||||
struct VRController
|
||||
{
|
||||
enum class kButton : uint8_t
|
||||
@@ -155,6 +163,7 @@ public:
|
||||
float display_density = 1.f;
|
||||
float zoom = 1.f;
|
||||
int idle_ms = 100;
|
||||
pp::platform::PlatformServices* platform_services_ = nullptr;
|
||||
|
||||
#if defined(__IOS__) && defined(__OBJC__)
|
||||
GameViewController* ios_view;
|
||||
@@ -174,15 +183,53 @@ public:
|
||||
bool clipboard_set_text(const std::string& s);
|
||||
void pick_image(std::function<void(std::string path)> callback);
|
||||
void pick_file(std::vector<std::string> types, std::function<void(std::string path)> callback);
|
||||
#if __IOS__ || __WEB__
|
||||
void pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string path)> writer, std::function<void(const std::string& path, bool saved)> callback);
|
||||
#else
|
||||
void pick_file_save(std::vector<std::string> types, std::function<void(std::string path)> callback);
|
||||
#endif
|
||||
[[nodiscard]] bool supports_working_directory_picker() const;
|
||||
[[nodiscard]] std::string format_working_directory_path(std::string_view path) const;
|
||||
[[nodiscard]] bool uses_prepared_file_writes() const;
|
||||
[[nodiscard]] bool uses_work_directory_document_export_collections() const;
|
||||
[[nodiscard]] bool disables_network_tls_verification() const;
|
||||
[[nodiscard]] bool uses_ppbr_export_data_directory_override() const;
|
||||
[[nodiscard]] bool platform_supports_sonarpen() const;
|
||||
void start_platform_sonarpen();
|
||||
[[nodiscard]] int default_canvas_resolution() const;
|
||||
[[nodiscard]] bool draws_canvas_tip_for_input(kEventSource source, kEventType type) const;
|
||||
[[nodiscard]] float adjust_canvas_input_pressure(float pressure) const;
|
||||
void pick_dir(std::function<void(std::string path)> callback);
|
||||
void display_file(std::string path);
|
||||
void share_file(std::string path);
|
||||
void request_app_close();
|
||||
[[nodiscard]] bool start_platform_vr_mode();
|
||||
void stop_platform_vr_mode();
|
||||
void attach_ui_thread();
|
||||
void detach_ui_thread();
|
||||
void acquire_render_context();
|
||||
void release_render_context();
|
||||
void present_render_context();
|
||||
void bind_default_render_target();
|
||||
void bind_main_render_target();
|
||||
void apply_render_platform_hints();
|
||||
void install_render_debug_callback();
|
||||
void begin_render_capture_frame();
|
||||
void end_render_capture_frame();
|
||||
[[nodiscard]] bool platform_deletes_recorded_files_on_clear();
|
||||
void clear_platform_recorded_files(std::string path);
|
||||
void publish_exported_image(std::string path);
|
||||
void flush_platform_storage();
|
||||
[[nodiscard]] std::vector<std::string> document_browse_roots() const;
|
||||
void save_platform_ui_state();
|
||||
[[nodiscard]] bool platform_enables_live_asset_reloading();
|
||||
void update_platform_frame(float delta_time_seconds);
|
||||
void report_rendered_frames(int frames);
|
||||
void save_prepared_file(
|
||||
std::string path,
|
||||
std::string suggested_name,
|
||||
std::function<void(const std::string& path, bool saved)> callback);
|
||||
void set_platform_services(pp::platform::PlatformServices* services) noexcept;
|
||||
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
|
||||
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths();
|
||||
void showKeyboard();
|
||||
void hideKeyboard();
|
||||
void initLog();
|
||||
@@ -248,6 +295,8 @@ public:
|
||||
void dialog_usermanual();
|
||||
void dialog_changelog();
|
||||
void dialog_about();
|
||||
void save_document(pp::app::DocumentSaveIntent intent);
|
||||
void continue_document_workflow_after_optional_save(std::function<void()> action);
|
||||
void dialog_newdoc();
|
||||
void dialog_save();
|
||||
void dialog_save_ver();
|
||||
@@ -327,20 +376,35 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
unique,
|
||||
0U,
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
render_tasklist.size(),
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !render_tasklist.empty())
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
render_tasklist.erase(std::remove_if(render_tasklist.begin(), render_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist.end());
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
@@ -351,19 +415,27 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_render_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
false,
|
||||
0U,
|
||||
render_running,
|
||||
true,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
if (render_running)
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
}
|
||||
|
||||
@@ -399,20 +471,35 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
unique,
|
||||
0U,
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
ui_tasklist.size(),
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (unique && !ui_tasklist.empty())
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
ui_tasklist.erase(std::remove_if(ui_tasklist.begin(), ui_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist.end());
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
@@ -423,20 +510,29 @@ public:
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
if (is_ui_thread())
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
false,
|
||||
0U,
|
||||
ui_running,
|
||||
true,
|
||||
true);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
if (ui_running)
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
if (dispatch.request_redraw)
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/document_cloud.h"
|
||||
#include "legacy_cloud_services.h"
|
||||
#include "util.h"
|
||||
#include "node_progress_bar.h"
|
||||
#include "node_dialog_cloud.h"
|
||||
|
||||
void App::cloud_upload()
|
||||
{
|
||||
if (!canvas)
|
||||
return;
|
||||
if (Canvas::I->m_newdoc)
|
||||
{
|
||||
message_box("Warning", "This document needs to be saved before upload.");
|
||||
}
|
||||
else
|
||||
{
|
||||
auto upload_thread = [this] {
|
||||
BT_SetTerminate();
|
||||
const bool has_canvas = canvas != nullptr;
|
||||
const auto plan = pp::app::plan_cloud_upload(
|
||||
has_canvas,
|
||||
has_canvas && Canvas::I->m_newdoc,
|
||||
has_canvas && Canvas::I->m_unsaved);
|
||||
|
||||
if (Canvas::I->m_unsaved)
|
||||
{
|
||||
Canvas::I->project_save_thread(doc_path, true);
|
||||
}
|
||||
|
||||
auto pb = show_progress("Uploading");
|
||||
|
||||
upload(doc_path, doc_filename, [this,pb](float p){
|
||||
pb->set_progress(p);
|
||||
});
|
||||
|
||||
pb->destroy();
|
||||
message_box("Success", "This document has been succesfully uploaded.");
|
||||
};
|
||||
|
||||
auto m = message_box("Publish document", "Would you like to upload to the public domain?");
|
||||
m->btn_ok->m_text->set_text("Yes");
|
||||
m->btn_cancel->m_text->set_text("No");
|
||||
m->btn_ok->on_click = [this, m, upload_thread](Node*) {
|
||||
std::thread(upload_thread).detach();
|
||||
m->destroy();
|
||||
};
|
||||
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
|
||||
m->destroy();
|
||||
};
|
||||
}
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_upload_plan(*this, plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud upload action failed: %s", status.message);
|
||||
}
|
||||
|
||||
void App::cloud_upload_all()
|
||||
@@ -51,70 +23,18 @@ void App::cloud_upload_all()
|
||||
BT_SetTerminate();
|
||||
|
||||
auto names = Asset::list_files(data_path, ".*\\.ppi");
|
||||
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
|
||||
|
||||
gl_state gl;
|
||||
std::shared_ptr<NodeProgressBar> pb;
|
||||
if (layout.m_loaded)
|
||||
pb = show_progress("Export Pano Image", names.size());
|
||||
|
||||
for (const auto& n : names)
|
||||
{
|
||||
std::string path = data_path + "/" + n;
|
||||
upload(path);
|
||||
|
||||
if (layout.m_loaded)
|
||||
pb->increment();
|
||||
}
|
||||
|
||||
if (layout.m_loaded)
|
||||
pb->destroy();
|
||||
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud bulk upload action failed: %s", status.message);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void App::cloud_browse()
|
||||
{
|
||||
if (!canvas)
|
||||
return;
|
||||
|
||||
// load thumbnail test
|
||||
auto dialog = std::make_shared<NodeDialogCloud>();
|
||||
dialog->set_manager(&layout);
|
||||
dialog->init();
|
||||
dialog->create();
|
||||
dialog->loaded();
|
||||
|
||||
layout[main_id]->add_child(dialog);
|
||||
|
||||
dialog->btn_ok->on_click = [this, dialog](Node*)
|
||||
{
|
||||
if (dialog->selected_file.empty())
|
||||
return;
|
||||
dialog->destroy();
|
||||
std::thread([this, dialog] {
|
||||
BT_SetTerminate();
|
||||
|
||||
auto* m = layout[main_id]->add_child<NodeMessageBox>();
|
||||
m->m_title->set_text("Downloading");
|
||||
m->m_message->set_text("Download in progress");
|
||||
std::string url = "https://panopainter.com/cloud/cloud-dwl.php?file=" + dialog->selected_file;
|
||||
download(url, dialog->selected_path, [this,m](float p){
|
||||
static char progress[256];
|
||||
sprintf(progress, "Download in progress %.2f%%", p * 100.f);
|
||||
m->m_message->set_text(progress);
|
||||
});
|
||||
|
||||
canvas->reset_camera();
|
||||
layers->clear();
|
||||
|
||||
canvas->m_canvas->project_open_thread(dialog->selected_path);
|
||||
|
||||
doc_name = dialog->selected_name;
|
||||
title_update();
|
||||
for (auto& l : canvas->m_canvas->m_layers)
|
||||
layers->add_layer(l->m_name.c_str(), false);
|
||||
ActionManager::clear();
|
||||
m->destroy();
|
||||
}).detach();
|
||||
};
|
||||
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_browse_action(*this, browse_plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud browse action failed: %s", status.message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,70 @@
|
||||
#include "pch.h"
|
||||
#include "app_core/command_convert.h"
|
||||
#include "app.h"
|
||||
#include "canvas.h"
|
||||
#include "legacy_ui_gl_dispatch.h"
|
||||
#include "log.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_convert_command_state()
|
||||
{
|
||||
const auto status = pp::renderer::gl::apply_panopainter_convert_command_state(
|
||||
pp::renderer::gl::OpenGlConvertCommandStateDispatch {
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
.blend_func = pp::legacy::ui_gl::set_opengl_blend_func,
|
||||
.blend_equation = pp::legacy::ui_gl::set_opengl_blend_equation,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL convert command state failed: %s", status.message);
|
||||
}
|
||||
|
||||
class LegacyCommandConvertServices final : public pp::app::CommandConvertServices {
|
||||
public:
|
||||
void apply_renderer_state() override
|
||||
{
|
||||
apply_convert_command_state();
|
||||
}
|
||||
|
||||
void create_canvas(int canvas_resolution) override
|
||||
{
|
||||
command_canvas = new Canvas;
|
||||
command_canvas->create(canvas_resolution, canvas_resolution);
|
||||
}
|
||||
|
||||
void open_project(std::string_view project_path) override
|
||||
{
|
||||
if (command_canvas)
|
||||
command_canvas->project_open_thread(std::string(project_path));
|
||||
}
|
||||
|
||||
void export_equirectangular(std::string_view output_path) override
|
||||
{
|
||||
if (command_canvas)
|
||||
command_canvas->export_equirectangular_thread(std::string(output_path));
|
||||
}
|
||||
|
||||
private:
|
||||
Canvas* command_canvas = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void App::cmd_convert(std::string pano_path, std::string out_path)
|
||||
{
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glBlendEquation(GL_FUNC_ADD);
|
||||
|
||||
Canvas* canvas = new Canvas;
|
||||
canvas->create(CANVAS_RES, CANVAS_RES);
|
||||
canvas->project_open_thread(pano_path);
|
||||
canvas->export_equirectangular_thread(out_path);
|
||||
const auto plan = pp::app::plan_command_convert(
|
||||
pano_path,
|
||||
out_path,
|
||||
default_canvas_resolution());
|
||||
if (!plan) {
|
||||
LOG("Convert command rejected: %s", plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
LegacyCommandConvertServices services;
|
||||
const auto status = pp::app::execute_command_convert_plan(plan.value(), services);
|
||||
if (!status.ok())
|
||||
LOG("Convert command failed: %s", status.message);
|
||||
}
|
||||
|
||||
126
src/app_core/about_menu.h
Normal file
126
src/app_core/about_menu.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class AboutMenuCommand {
|
||||
help_guide,
|
||||
about_app,
|
||||
whats_new,
|
||||
induce_crash,
|
||||
performance_test,
|
||||
};
|
||||
|
||||
enum class AboutMenuAction {
|
||||
show_user_manual,
|
||||
show_about_dialog,
|
||||
show_whats_new_dialog,
|
||||
trigger_crash_test,
|
||||
run_performance_test,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
struct AboutMenuPlan {
|
||||
AboutMenuCommand command = AboutMenuCommand::help_guide;
|
||||
AboutMenuAction action = AboutMenuAction::show_user_manual;
|
||||
std::string label;
|
||||
bool closes_root_popup = true;
|
||||
bool requires_canvas = false;
|
||||
int performance_iterations = 0;
|
||||
int performance_updates_per_iteration = 0;
|
||||
};
|
||||
|
||||
class AboutMenuServices {
|
||||
public:
|
||||
virtual ~AboutMenuServices() = default;
|
||||
|
||||
virtual void show_user_manual() = 0;
|
||||
virtual void show_about_dialog() = 0;
|
||||
virtual void show_whats_new_dialog() = 0;
|
||||
virtual void trigger_crash_test() = 0;
|
||||
virtual void run_performance_test(const AboutMenuPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AboutMenuPlan plan_about_menu_command(
|
||||
AboutMenuCommand command,
|
||||
int version_major,
|
||||
int version_minor,
|
||||
int version_fix,
|
||||
bool diagnostics_available = true,
|
||||
bool has_canvas = true)
|
||||
{
|
||||
AboutMenuPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case AboutMenuCommand::help_guide:
|
||||
plan.action = AboutMenuAction::show_user_manual;
|
||||
plan.label = "Help Guide";
|
||||
break;
|
||||
case AboutMenuCommand::about_app:
|
||||
plan.action = AboutMenuAction::show_about_dialog;
|
||||
plan.label = "About PanoPainter";
|
||||
break;
|
||||
case AboutMenuCommand::whats_new:
|
||||
plan.action = AboutMenuAction::show_whats_new_dialog;
|
||||
plan.label = "What's new in "
|
||||
+ std::to_string(version_major)
|
||||
+ "."
|
||||
+ std::to_string(version_minor)
|
||||
+ "."
|
||||
+ std::to_string(version_fix)
|
||||
+ "?";
|
||||
break;
|
||||
case AboutMenuCommand::induce_crash:
|
||||
plan.label = "Induce crash";
|
||||
plan.action = diagnostics_available
|
||||
? AboutMenuAction::trigger_crash_test
|
||||
: AboutMenuAction::no_op_unavailable;
|
||||
plan.closes_root_popup = diagnostics_available;
|
||||
break;
|
||||
case AboutMenuCommand::performance_test:
|
||||
plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)";
|
||||
plan.requires_canvas = true;
|
||||
plan.performance_iterations = 100;
|
||||
plan.performance_updates_per_iteration = 10;
|
||||
plan.action = has_canvas
|
||||
? AboutMenuAction::run_performance_test
|
||||
: AboutMenuAction::no_op_unavailable;
|
||||
plan.closes_root_popup = has_canvas;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_about_menu_plan(
|
||||
const AboutMenuPlan& plan,
|
||||
AboutMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case AboutMenuAction::show_user_manual:
|
||||
services.show_user_manual();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::show_about_dialog:
|
||||
services.show_about_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::show_whats_new_dialog:
|
||||
services.show_whats_new_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::trigger_crash_test:
|
||||
services.trigger_crash_test();
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::run_performance_test:
|
||||
services.run_performance_test(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case AboutMenuAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown about menu action");
|
||||
}
|
||||
|
||||
}
|
||||
118
src/app_core/app_dialog.h
Normal file
118
src/app_core/app_dialog.h
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class AppDialogKind {
|
||||
progress,
|
||||
message,
|
||||
input,
|
||||
};
|
||||
|
||||
struct AppProgressDialogPlan {
|
||||
std::string title;
|
||||
int total = 0;
|
||||
int count = 0;
|
||||
float progress_fraction = 0.0F;
|
||||
};
|
||||
|
||||
struct AppMessageDialogPlan {
|
||||
std::string title;
|
||||
std::string message;
|
||||
std::string ok_caption = "Ok";
|
||||
std::string cancel_caption = "Cancel";
|
||||
bool show_cancel = false;
|
||||
};
|
||||
|
||||
struct AppInputDialogPlan {
|
||||
std::string title;
|
||||
std::string field_name;
|
||||
std::string ok_caption = "Ok";
|
||||
};
|
||||
|
||||
class AppDialog {
|
||||
public:
|
||||
virtual ~AppDialog() = default;
|
||||
[[nodiscard]] virtual AppDialogKind kind() const noexcept = 0;
|
||||
};
|
||||
|
||||
class AppProgressDialog : public AppDialog {
|
||||
public:
|
||||
~AppProgressDialog() override = default;
|
||||
};
|
||||
|
||||
class AppMessageDialog : public AppDialog {
|
||||
public:
|
||||
~AppMessageDialog() override = default;
|
||||
};
|
||||
|
||||
class AppInputDialog : public AppDialog {
|
||||
public:
|
||||
~AppInputDialog() override = default;
|
||||
};
|
||||
|
||||
class AppDialogFactory {
|
||||
public:
|
||||
virtual ~AppDialogFactory() = default;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppProgressDialog> show_progress_dialog(
|
||||
const AppProgressDialogPlan& plan) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppMessageDialog> show_message_dialog(
|
||||
const AppMessageDialogPlan& plan) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<AppInputDialog> show_input_dialog(
|
||||
const AppInputDialogPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_app_progress_dialog(
|
||||
std::string_view title,
|
||||
int total) noexcept
|
||||
{
|
||||
return {
|
||||
std::string(title),
|
||||
total < 0 ? 0 : total,
|
||||
0,
|
||||
0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_app_message_dialog(
|
||||
std::string_view title,
|
||||
std::string_view message,
|
||||
bool show_cancel,
|
||||
std::string_view ok_caption = "Ok",
|
||||
std::string_view cancel_caption = "Cancel")
|
||||
{
|
||||
return {
|
||||
std::string(title),
|
||||
std::string(message),
|
||||
std::string(ok_caption),
|
||||
std::string(cancel_caption),
|
||||
show_cancel,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppInputDialogPlan> plan_app_input_dialog(
|
||||
std::string_view title,
|
||||
std::string_view field_name,
|
||||
std::string_view ok_caption)
|
||||
{
|
||||
if (ok_caption.empty()) {
|
||||
return pp::foundation::Result<AppInputDialogPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("input dialog ok caption must not be empty"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppInputDialogPlan>::success({
|
||||
std::string(title),
|
||||
std::string(field_name),
|
||||
std::string(ok_caption),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
240
src/app_core/app_frame.h
Normal file
240
src/app_core/app_frame.h
Normal file
@@ -0,0 +1,240 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppInitialSurfacePlan {
|
||||
float width = 960.0F;
|
||||
float height = 540.0F;
|
||||
};
|
||||
|
||||
struct AppFrameUpdatePlan {
|
||||
bool update_frame = false;
|
||||
bool update_layouts = false;
|
||||
bool refresh_canvas_toolbar = false;
|
||||
};
|
||||
|
||||
struct AppFrameDrawPlan {
|
||||
bool draw_canvas_stroke = false;
|
||||
bool draw_vr_ui = false;
|
||||
bool draw_main_ui = true;
|
||||
bool reset_redraw = true;
|
||||
};
|
||||
|
||||
struct AppFrameTickPlan {
|
||||
bool tick_designer_layout = false;
|
||||
bool tick_main_layout = false;
|
||||
};
|
||||
|
||||
struct AppResizePlan {
|
||||
float width = 0.0F;
|
||||
float height = 0.0F;
|
||||
int render_target_width = 0;
|
||||
int render_target_height = 0;
|
||||
bool recreate_ui_render_target = true;
|
||||
bool request_redraw = true;
|
||||
};
|
||||
|
||||
struct AppUiObserverRect {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
float width = 0.0F;
|
||||
float height = 0.0F;
|
||||
};
|
||||
|
||||
struct AppUiObserverParentClip {
|
||||
AppUiObserverRect clip;
|
||||
float padding_top = 0.0F;
|
||||
float padding_right = 0.0F;
|
||||
float padding_bottom = 0.0F;
|
||||
float padding_left = 0.0F;
|
||||
};
|
||||
|
||||
struct AppUiObserverPlan {
|
||||
bool draw_node = false;
|
||||
bool notify_enter_screen = false;
|
||||
bool notify_leave_screen = false;
|
||||
bool next_on_screen = false;
|
||||
AppUiObserverRect visible_clip;
|
||||
std::int32_t scissor_x = 0;
|
||||
std::int32_t scissor_y = 0;
|
||||
std::int32_t scissor_width = 0;
|
||||
std::int32_t scissor_height = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppInitialSurfacePlan plan_app_initial_surface() noexcept
|
||||
{
|
||||
return AppInitialSurfacePlan {
|
||||
.width = 1920.0F / 2.0F,
|
||||
.height = 1080.0F / 2.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameUpdatePlan plan_app_frame_update(bool redraw, bool animate) noexcept
|
||||
{
|
||||
const bool update_frame = redraw || animate;
|
||||
return AppFrameUpdatePlan {
|
||||
.update_frame = update_frame,
|
||||
.update_layouts = update_frame,
|
||||
.refresh_canvas_toolbar = update_frame,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameDrawPlan plan_app_frame_draw(
|
||||
bool has_canvas_node,
|
||||
bool has_canvas_document,
|
||||
bool vr_active,
|
||||
bool ui_visible,
|
||||
bool vr_only) noexcept
|
||||
{
|
||||
return AppFrameDrawPlan {
|
||||
.draw_canvas_stroke = has_canvas_node && has_canvas_document,
|
||||
.draw_vr_ui = vr_active && ui_visible,
|
||||
.draw_main_ui = !vr_only,
|
||||
.reset_redraw = true,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppFrameTickPlan plan_app_frame_tick(
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout) noexcept
|
||||
{
|
||||
return AppFrameTickPlan {
|
||||
.tick_designer_layout = has_designer_layout,
|
||||
.tick_main_layout = has_main_layout,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppResizePlan> plan_app_resize(float width, float height)
|
||||
{
|
||||
if (!std::isfinite(width) || !std::isfinite(height)) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("resize dimensions must be finite"));
|
||||
}
|
||||
|
||||
if (width < 1.0F || height < 1.0F) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("resize dimensions must be positive"));
|
||||
}
|
||||
|
||||
if (width > static_cast<float>(std::numeric_limits<int>::max())
|
||||
|| height > static_cast<float>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<AppResizePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("resize dimensions exceed integer range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppResizePlan>::success(AppResizePlan {
|
||||
.width = width,
|
||||
.height = height,
|
||||
.render_target_width = static_cast<int>(width),
|
||||
.render_target_height = static_cast<int>(height),
|
||||
.recreate_ui_render_target = true,
|
||||
.request_redraw = true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiObserverRect intersect_app_ui_observer_rect(
|
||||
AppUiObserverRect a,
|
||||
AppUiObserverRect b) noexcept
|
||||
{
|
||||
const float x0 = a.x > b.x ? a.x : b.x;
|
||||
const float y0 = a.y > b.y ? a.y : b.y;
|
||||
const float x1 = (a.x + a.width) < (b.x + b.width) ? (a.x + a.width) : (b.x + b.width);
|
||||
const float y1 = (a.y + a.height) < (b.y + b.height) ? (a.y + a.height) : (b.y + b.height);
|
||||
return AppUiObserverRect {
|
||||
.x = x0,
|
||||
.y = y0,
|
||||
.width = x1 - x0,
|
||||
.height = y1 - y0,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiObserverPlan> plan_app_ui_observer(
|
||||
bool has_node,
|
||||
bool display,
|
||||
bool was_on_screen,
|
||||
AppUiObserverRect node_clip,
|
||||
std::span<const AppUiObserverParentClip> parent_clips,
|
||||
float surface_height,
|
||||
float zoom,
|
||||
float offset_x,
|
||||
float offset_y)
|
||||
{
|
||||
if (!has_node || !display) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = false,
|
||||
.next_on_screen = was_on_screen,
|
||||
.visible_clip = node_clip,
|
||||
});
|
||||
}
|
||||
|
||||
const auto finite_rect = [](AppUiObserverRect rect) noexcept {
|
||||
return std::isfinite(rect.x) && std::isfinite(rect.y)
|
||||
&& std::isfinite(rect.width) && std::isfinite(rect.height);
|
||||
};
|
||||
|
||||
if (!finite_rect(node_clip) || !std::isfinite(surface_height)
|
||||
|| !std::isfinite(zoom) || !std::isfinite(offset_x) || !std::isfinite(offset_y)) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer geometry must be finite"));
|
||||
}
|
||||
|
||||
if (surface_height < 1.0F || zoom <= 0.0F) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer surface height and zoom must be positive"));
|
||||
}
|
||||
|
||||
AppUiObserverRect visible = node_clip;
|
||||
for (const auto& parent : parent_clips) {
|
||||
if (!finite_rect(parent.clip)
|
||||
|| !std::isfinite(parent.padding_top)
|
||||
|| !std::isfinite(parent.padding_right)
|
||||
|| !std::isfinite(parent.padding_bottom)
|
||||
|| !std::isfinite(parent.padding_left)) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI observer parent geometry must be finite"));
|
||||
}
|
||||
|
||||
const AppUiObserverRect padded {
|
||||
.x = parent.clip.x + parent.padding_left,
|
||||
.y = parent.clip.y + parent.padding_top,
|
||||
.width = parent.clip.width - parent.padding_right - parent.padding_left,
|
||||
.height = parent.clip.height - parent.padding_bottom - parent.padding_top,
|
||||
};
|
||||
visible = intersect_app_ui_observer_rect(visible, padded);
|
||||
}
|
||||
|
||||
if (visible.width <= 0.0F || visible.height <= 0.0F) {
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = false,
|
||||
.notify_leave_screen = was_on_screen,
|
||||
.next_on_screen = false,
|
||||
.visible_clip = visible,
|
||||
});
|
||||
}
|
||||
|
||||
const float projected_x = (visible.x - 1.0F) * zoom;
|
||||
const float projected_y = (surface_height / zoom - visible.y - visible.height - 1.0F) * zoom;
|
||||
const float projected_width = (visible.width + 2.0F) * zoom;
|
||||
const float projected_height = (visible.height + 2.0F) * zoom;
|
||||
|
||||
return pp::foundation::Result<AppUiObserverPlan>::success(AppUiObserverPlan {
|
||||
.draw_node = true,
|
||||
.notify_enter_screen = !was_on_screen,
|
||||
.notify_leave_screen = false,
|
||||
.next_on_screen = true,
|
||||
.visible_clip = visible,
|
||||
.scissor_x = static_cast<std::int32_t>(std::floor(projected_x + offset_x)),
|
||||
.scissor_y = static_cast<std::int32_t>(std::floor(projected_y + offset_y)),
|
||||
.scissor_width = static_cast<std::int32_t>(std::ceil(projected_width)),
|
||||
.scissor_height = static_cast<std::int32_t>(std::ceil(projected_height)),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
209
src/app_core/app_input.h
Normal file
209
src/app_core/app_input.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppPointerDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_designer_first = false;
|
||||
bool dispatch_main_if_not_consumed = false;
|
||||
float normalized_x = 0.0F;
|
||||
float normalized_y = 0.0F;
|
||||
};
|
||||
|
||||
struct AppInputDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
};
|
||||
|
||||
struct AppGestureDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
float normalized_x = 0.0F;
|
||||
float normalized_y = 0.0F;
|
||||
float distance = 0.0F;
|
||||
float distance_delta = 0.0F;
|
||||
float position_delta_x = 0.0F;
|
||||
float position_delta_y = 0.0F;
|
||||
};
|
||||
|
||||
struct AppKeyDispatchPlan {
|
||||
bool request_redraw = true;
|
||||
bool dispatch_main = false;
|
||||
bool set_key_down = false;
|
||||
bool sync_vr_camera_rotation = false;
|
||||
};
|
||||
|
||||
struct AppUiVisibilityTogglePlan {
|
||||
bool next_ui_visible = true;
|
||||
std::size_t first_panel_child_index = 1;
|
||||
std::size_t panel_child_count = 0;
|
||||
};
|
||||
|
||||
struct AppStylusAttachPlan {
|
||||
bool set_has_stylus = true;
|
||||
bool enable_canvas_touch_lock = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_input_zoom(float zoom)
|
||||
{
|
||||
if (!std::isfinite(zoom) || zoom <= 0.0F) {
|
||||
return pp::foundation::Status::invalid_argument("input zoom must be finite and positive");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppPointerDispatchPlan> plan_app_pointer_dispatch(
|
||||
float x,
|
||||
float y,
|
||||
float zoom,
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout)
|
||||
{
|
||||
const auto zoom_status = validate_input_zoom(zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::failure(zoom_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(x) || !std::isfinite(y)) {
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("input coordinates must be finite"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppPointerDispatchPlan>::success(AppPointerDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_designer_first = has_designer_layout,
|
||||
.dispatch_main_if_not_consumed = has_main_layout,
|
||||
.normalized_x = x / zoom,
|
||||
.normalized_y = y / zoom,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppPointerDispatchPlan plan_app_mouse_cancel_dispatch(
|
||||
bool has_designer_layout,
|
||||
bool has_main_layout) noexcept
|
||||
{
|
||||
return AppPointerDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_designer_first = has_designer_layout,
|
||||
.dispatch_main_if_not_consumed = has_main_layout,
|
||||
.normalized_x = 0.0F,
|
||||
.normalized_y = 0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppInputDispatchPlan plan_app_main_input_dispatch(bool has_main_layout) noexcept
|
||||
{
|
||||
return AppInputDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppGestureDispatchPlan> plan_app_gesture_dispatch(
|
||||
float x0,
|
||||
float y0,
|
||||
float x1,
|
||||
float y1,
|
||||
float previous_x0,
|
||||
float previous_y0,
|
||||
float previous_x1,
|
||||
float previous_y1,
|
||||
float zoom,
|
||||
bool has_main_layout)
|
||||
{
|
||||
const auto zoom_status = validate_input_zoom(zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::failure(zoom_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(x0) || !std::isfinite(y0) || !std::isfinite(x1) || !std::isfinite(y1)
|
||||
|| !std::isfinite(previous_x0) || !std::isfinite(previous_y0)
|
||||
|| !std::isfinite(previous_x1) || !std::isfinite(previous_y1)) {
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("gesture coordinates must be finite"));
|
||||
}
|
||||
|
||||
const float midpoint_x = (x0 + x1) * 0.5F;
|
||||
const float midpoint_y = (y0 + y1) * 0.5F;
|
||||
const float previous_midpoint_x = (previous_x0 + previous_x1) * 0.5F;
|
||||
const float previous_midpoint_y = (previous_y0 + previous_y1) * 0.5F;
|
||||
const float dx = x1 - x0;
|
||||
const float dy = y1 - y0;
|
||||
const float previous_dx = previous_x1 - previous_x0;
|
||||
const float previous_dy = previous_y1 - previous_y0;
|
||||
const float distance = std::sqrt(dx * dx + dy * dy);
|
||||
const float previous_distance = std::sqrt(previous_dx * previous_dx + previous_dy * previous_dy);
|
||||
|
||||
return pp::foundation::Result<AppGestureDispatchPlan>::success(AppGestureDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.normalized_x = midpoint_x / zoom,
|
||||
.normalized_y = midpoint_y / zoom,
|
||||
.distance = distance,
|
||||
.distance_delta = distance - previous_distance,
|
||||
.position_delta_x = midpoint_x - previous_midpoint_x,
|
||||
.position_delta_y = midpoint_y - previous_midpoint_y,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_down_dispatch(
|
||||
bool has_main_layout,
|
||||
bool is_spacebar,
|
||||
bool vr_active) noexcept
|
||||
{
|
||||
return AppKeyDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.set_key_down = true,
|
||||
.sync_vr_camera_rotation = is_spacebar && vr_active,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppKeyDispatchPlan plan_app_key_up_dispatch(bool has_main_layout) noexcept
|
||||
{
|
||||
return AppKeyDispatchPlan {
|
||||
.request_redraw = true,
|
||||
.dispatch_main = has_main_layout,
|
||||
.set_key_down = false,
|
||||
.sync_vr_camera_rotation = false,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiVisibilityTogglePlan> plan_app_ui_visibility_toggle(
|
||||
bool current_ui_visible,
|
||||
bool has_main_layout,
|
||||
std::size_t main_child_count,
|
||||
std::size_t panel_child_count)
|
||||
{
|
||||
if (!has_main_layout) {
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI toggle requires a main layout"));
|
||||
}
|
||||
|
||||
if (main_child_count <= 1U) {
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI toggle requires a panel container child"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppUiVisibilityTogglePlan>::success(AppUiVisibilityTogglePlan {
|
||||
.next_ui_visible = !current_ui_visible,
|
||||
.first_panel_child_index = 1U,
|
||||
.panel_child_count = panel_child_count,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppStylusAttachPlan plan_app_stylus_attach(bool has_canvas) noexcept
|
||||
{
|
||||
return AppStylusAttachPlan {
|
||||
.set_has_stylus = true,
|
||||
.enable_canvas_touch_lock = has_canvas,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
195
src/app_core/app_preferences.h
Normal file
195
src/app_core/app_preferences.h
Normal file
@@ -0,0 +1,195 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class InterfaceDirection {
|
||||
left_to_right,
|
||||
right_to_left,
|
||||
};
|
||||
|
||||
enum class TimelapseRecordingAction {
|
||||
no_op,
|
||||
start_recording,
|
||||
stop_recording,
|
||||
};
|
||||
|
||||
struct ScaleApplicationPlan {
|
||||
float scale = 1.0F;
|
||||
float display_density = 1.0F;
|
||||
float font_scale = 1.0F;
|
||||
};
|
||||
|
||||
struct ScaleOptionSelection {
|
||||
bool has_selection = false;
|
||||
std::size_t index = 0;
|
||||
};
|
||||
|
||||
struct InterfaceDirectionPlan {
|
||||
InterfaceDirection direction = InterfaceDirection::left_to_right;
|
||||
};
|
||||
|
||||
struct TimelapsePreferencePlan {
|
||||
bool enabled = true;
|
||||
TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op;
|
||||
};
|
||||
|
||||
struct StoredIntegerPreferencePlan {
|
||||
int value = 0;
|
||||
};
|
||||
|
||||
struct StoredBooleanPreferencePlan {
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
class AppPreferenceServices {
|
||||
public:
|
||||
virtual ~AppPreferenceServices() = default;
|
||||
|
||||
virtual void apply_ui_scale(const ScaleApplicationPlan& plan) = 0;
|
||||
virtual void apply_viewport_scale(const ScaleApplicationPlan& plan) = 0;
|
||||
virtual void apply_interface_direction(const InterfaceDirectionPlan& plan) = 0;
|
||||
virtual bool apply_vr_mode_preference(const StoredBooleanPreferencePlan& plan) = 0;
|
||||
virtual void apply_vr_controllers_preference(const StoredBooleanPreferencePlan& plan) = 0;
|
||||
virtual void apply_timelapse_preference(const TimelapsePreferencePlan& plan) = 0;
|
||||
virtual void apply_canvas_cursor_mode(const StoredIntegerPreferencePlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale(
|
||||
float requested_scale,
|
||||
float display_density) noexcept
|
||||
{
|
||||
return {
|
||||
requested_scale,
|
||||
display_density,
|
||||
requested_scale * display_density,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale(
|
||||
float requested_scale,
|
||||
float display_density = 1.0F) noexcept
|
||||
{
|
||||
return {
|
||||
requested_scale,
|
||||
display_density,
|
||||
requested_scale * display_density,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection(
|
||||
float current_scale,
|
||||
std::span<const float> options) noexcept
|
||||
{
|
||||
ScaleOptionSelection selection;
|
||||
for (std::size_t index = 0; index < options.size(); ++index) {
|
||||
if (current_scale >= options[index]) {
|
||||
selection.has_selection = true;
|
||||
selection.index = index;
|
||||
}
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept
|
||||
{
|
||||
return {
|
||||
right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference(
|
||||
bool enabled,
|
||||
bool recording_running) noexcept
|
||||
{
|
||||
if (enabled && !recording_running) {
|
||||
return { enabled, TimelapseRecordingAction::start_recording };
|
||||
}
|
||||
if (!enabled && recording_running) {
|
||||
return { enabled, TimelapseRecordingAction::stop_recording };
|
||||
}
|
||||
return { enabled, TimelapseRecordingAction::no_op };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference(
|
||||
bool enabled) noexcept
|
||||
{
|
||||
return { enabled };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_mode_preference(bool enabled) noexcept
|
||||
{
|
||||
return { enabled };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
|
||||
{
|
||||
return { mode };
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_ui_scale_preference(
|
||||
float requested_scale,
|
||||
float display_density,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_ui_scale(plan_ui_scale(requested_scale, display_density));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_viewport_scale_preference(
|
||||
float requested_scale,
|
||||
float display_density,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_viewport_scale(plan_viewport_scale(requested_scale, display_density));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_interface_direction_preference(
|
||||
bool right_to_left,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_interface_direction(plan_interface_direction(right_to_left));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_vr_mode_preference(
|
||||
bool enabled,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
if (!services.apply_vr_mode_preference(plan_vr_mode_preference(enabled))) {
|
||||
return pp::foundation::Status::invalid_argument("VR mode could not start");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_vr_controllers_preference(
|
||||
bool enabled,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_vr_controllers_preference(plan_vr_controllers_preference(enabled));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_timelapse_preference(
|
||||
bool enabled,
|
||||
bool recording_running,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_timelapse_preference(plan_timelapse_preference(enabled, recording_running));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_cursor_mode_preference(
|
||||
int mode,
|
||||
AppPreferenceServices& services)
|
||||
{
|
||||
services.apply_canvas_cursor_mode(plan_canvas_cursor_mode(mode));
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
23
src/app_core/app_shutdown.h
Normal file
23
src/app_core/app_shutdown.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppShutdownPlan {
|
||||
bool save_ui_state = true;
|
||||
bool terminate_stroke_preview_renderer = true;
|
||||
bool stop_recording = true;
|
||||
bool invalidate_textures = true;
|
||||
bool invalidate_shaders = true;
|
||||
bool unload_layouts = true;
|
||||
bool destroy_ui_render_target = true;
|
||||
bool destroy_face_plane = true;
|
||||
bool release_panel_nodes = true;
|
||||
bool clear_quick_mode_state = true;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppShutdownPlan plan_app_shutdown() noexcept
|
||||
{
|
||||
return AppShutdownPlan {};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
188
src/app_core/app_startup.h
Normal file
188
src/app_core/app_startup.h
Normal file
@@ -0,0 +1,188 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppStartupPlan {
|
||||
int previous_run_counter = 0;
|
||||
int next_run_counter = 1;
|
||||
bool save_preferences = true;
|
||||
bool start_timelapse = false;
|
||||
bool vr_controllers_enabled = true;
|
||||
bool show_license_warning = false;
|
||||
};
|
||||
|
||||
struct AppStartupResourcePlan {
|
||||
int ui_render_target_width = 0;
|
||||
int ui_render_target_height = 0;
|
||||
bool initialize_shaders = true;
|
||||
bool initialize_assets = true;
|
||||
bool initialize_layout = true;
|
||||
bool update_title = true;
|
||||
bool create_ui_render_target = true;
|
||||
};
|
||||
|
||||
class AppStartupServices {
|
||||
public:
|
||||
virtual ~AppStartupServices() = default;
|
||||
|
||||
virtual void store_run_counter(int value) = 0;
|
||||
virtual void save_preferences() = 0;
|
||||
virtual void start_timelapse_recording() = 0;
|
||||
virtual void apply_vr_controllers_enabled(bool enabled) = 0;
|
||||
virtual void show_license_warning() = 0;
|
||||
};
|
||||
|
||||
class AppStartupResourceServices {
|
||||
public:
|
||||
virtual ~AppStartupResourceServices() = default;
|
||||
|
||||
virtual void initialize_shaders() = 0;
|
||||
virtual void initialize_assets() = 0;
|
||||
virtual void initialize_layout() = 0;
|
||||
virtual void update_title() = 0;
|
||||
virtual void create_ui_render_target(int width, int height) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppStartupPlan> plan_app_startup(
|
||||
int current_run_counter,
|
||||
bool auto_timelapse_enabled,
|
||||
bool stored_vr_controllers_enabled,
|
||||
bool license_valid)
|
||||
{
|
||||
if (current_run_counter < 0) {
|
||||
return pp::foundation::Result<AppStartupPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("run counter must not be negative"));
|
||||
}
|
||||
|
||||
if (current_run_counter == std::numeric_limits<int>::max()) {
|
||||
return pp::foundation::Result<AppStartupPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("run counter would overflow"));
|
||||
}
|
||||
|
||||
AppStartupPlan plan;
|
||||
plan.previous_run_counter = current_run_counter;
|
||||
plan.next_run_counter = current_run_counter + 1;
|
||||
plan.start_timelapse = auto_timelapse_enabled;
|
||||
plan.vr_controllers_enabled = stored_vr_controllers_enabled;
|
||||
plan.show_license_warning = !license_valid;
|
||||
return pp::foundation::Result<AppStartupPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppStartupResourcePlan> plan_app_startup_resources(
|
||||
float ui_width,
|
||||
float ui_height)
|
||||
{
|
||||
if (!std::isfinite(ui_width) || !std::isfinite(ui_height)) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("startup resource dimensions must be finite"));
|
||||
}
|
||||
|
||||
if (ui_width < 1.0F || ui_height < 1.0F) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("startup resource dimensions must be positive"));
|
||||
}
|
||||
|
||||
if (ui_width > static_cast<float>(std::numeric_limits<int>::max())
|
||||
|| ui_height > static_cast<float>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("startup resource dimensions exceed integer range"));
|
||||
}
|
||||
|
||||
AppStartupResourcePlan plan;
|
||||
plan.ui_render_target_width = static_cast<int>(ui_width);
|
||||
plan.ui_render_target_height = static_cast<int>(ui_height);
|
||||
return pp::foundation::Result<AppStartupResourcePlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
services.store_run_counter(plan.next_run_counter);
|
||||
if (plan.save_preferences) {
|
||||
services.save_preferences();
|
||||
}
|
||||
if (plan.start_timelapse) {
|
||||
services.start_timelapse_recording();
|
||||
}
|
||||
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
|
||||
if (plan.show_license_warning) {
|
||||
services.show_license_warning();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_persistence_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
services.store_run_counter(plan.next_run_counter);
|
||||
if (plan.save_preferences) {
|
||||
services.save_preferences();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_runtime_plan(
|
||||
const AppStartupPlan& plan,
|
||||
AppStartupServices& services)
|
||||
{
|
||||
if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) {
|
||||
return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state");
|
||||
}
|
||||
|
||||
if (plan.start_timelapse) {
|
||||
services.start_timelapse_recording();
|
||||
}
|
||||
services.apply_vr_controllers_enabled(plan.vr_controllers_enabled);
|
||||
if (plan.show_license_warning) {
|
||||
services.show_license_warning();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_app_startup_resources(
|
||||
const AppStartupResourcePlan& plan,
|
||||
AppStartupResourceServices& services)
|
||||
{
|
||||
if (plan.create_ui_render_target
|
||||
&& (plan.ui_render_target_width <= 0 || plan.ui_render_target_height <= 0)) {
|
||||
return pp::foundation::Status::invalid_argument("startup resource plan has invalid UI render target size");
|
||||
}
|
||||
|
||||
if (plan.initialize_shaders) {
|
||||
services.initialize_shaders();
|
||||
}
|
||||
if (plan.initialize_assets) {
|
||||
services.initialize_assets();
|
||||
}
|
||||
if (plan.initialize_layout) {
|
||||
services.initialize_layout();
|
||||
}
|
||||
if (plan.update_title) {
|
||||
services.update_title();
|
||||
}
|
||||
if (plan.create_ui_render_target) {
|
||||
services.create_ui_render_target(plan.ui_render_target_width, plan.ui_render_target_height);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
170
src/app_core/app_status.h
Normal file
170
src/app_core/app_status.h
Normal file
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr std::array<int, 6> document_resolution_values {
|
||||
512,
|
||||
1024,
|
||||
1536,
|
||||
2048,
|
||||
4096,
|
||||
8192,
|
||||
};
|
||||
|
||||
inline constexpr std::array<std::string_view, 6> document_resolution_labels {
|
||||
"2K",
|
||||
"4K",
|
||||
"6K",
|
||||
"8K",
|
||||
"16K",
|
||||
"32K",
|
||||
};
|
||||
|
||||
struct RecordingFrameLabel {
|
||||
bool visible = false;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticsInput {
|
||||
bool framebuffer_fetch = false;
|
||||
bool float32_render_targets = false;
|
||||
bool float32_linear_filtering = false;
|
||||
bool float16_render_targets = false;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticIndicator {
|
||||
bool supported = false;
|
||||
std::string_view label;
|
||||
};
|
||||
|
||||
struct RendererDiagnosticsPlan {
|
||||
RendererDiagnosticIndicator framebuffer_fetch;
|
||||
RendererDiagnosticIndicator floating_point_targets;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<int> display_resolution_from_index(int index)
|
||||
{
|
||||
if (index < 0 || static_cast<std::size_t>(index) >= document_resolution_values.size()) {
|
||||
return pp::foundation::Result<int>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||
}
|
||||
return pp::foundation::Result<int>::success(
|
||||
document_resolution_values[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<std::size_t> document_resolution_to_index(int resolution)
|
||||
{
|
||||
for (std::size_t index = 0; index < document_resolution_values.size(); ++index) {
|
||||
if (document_resolution_values[index] == resolution) {
|
||||
return pp::foundation::Result<std::size_t>::success(index);
|
||||
}
|
||||
}
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution is not supported"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<std::string_view> document_resolution_label(int resolution)
|
||||
{
|
||||
const auto index = document_resolution_to_index(resolution);
|
||||
if (!index) {
|
||||
return pp::foundation::Result<std::string_view>::failure(index.status());
|
||||
}
|
||||
return pp::foundation::Result<std::string_view>::success(document_resolution_labels[index.value()]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_document_title(
|
||||
std::string_view document_name,
|
||||
bool has_unsaved_changes,
|
||||
int resolution)
|
||||
{
|
||||
const auto label = document_resolution_label(resolution);
|
||||
const auto resolution_label = label ? label.value() : std::string_view("unknown");
|
||||
std::string title = "Panodoc: ";
|
||||
title.append(document_name);
|
||||
if (has_unsaved_changes) {
|
||||
title.push_back('*');
|
||||
}
|
||||
title.append(" (");
|
||||
title.append(resolution_label);
|
||||
title.push_back(')');
|
||||
return title;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_dpi_label(float zoom)
|
||||
{
|
||||
char buffer[64] {};
|
||||
std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes)
|
||||
{
|
||||
char buffer[128] {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"History memory: %.2f Mb",
|
||||
static_cast<double>(bytes) / 1024.0 / 1024.0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label(
|
||||
bool is_recording,
|
||||
bool encoder_available,
|
||||
int encoded_frames)
|
||||
{
|
||||
if (!is_recording || !encoder_available) {
|
||||
return {};
|
||||
}
|
||||
|
||||
char buffer[128] {};
|
||||
std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames);
|
||||
return {
|
||||
true,
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline RendererDiagnosticsPlan plan_renderer_diagnostics(
|
||||
RendererDiagnosticsInput input) noexcept
|
||||
{
|
||||
RendererDiagnosticsPlan plan;
|
||||
plan.framebuffer_fetch = {
|
||||
input.framebuffer_fetch,
|
||||
"FBF",
|
||||
};
|
||||
|
||||
if (input.float32_linear_filtering) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F32L",
|
||||
};
|
||||
} else if (input.float32_render_targets) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F32",
|
||||
};
|
||||
} else if (input.float16_render_targets) {
|
||||
plan.floating_point_targets = {
|
||||
true,
|
||||
"F16",
|
||||
};
|
||||
} else {
|
||||
plan.floating_point_targets = {
|
||||
false,
|
||||
"",
|
||||
};
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
}
|
||||
200
src/app_core/app_thread.h
Normal file
200
src/app_core/app_thread.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct AppTaskDispatchPlan {
|
||||
bool execute_immediately = false;
|
||||
bool queue_task = false;
|
||||
bool remove_matching_unique_task = false;
|
||||
bool notify_worker = false;
|
||||
bool wait_for_completion = false;
|
||||
bool request_redraw = false;
|
||||
};
|
||||
|
||||
struct AppAsyncRedrawPlan {
|
||||
bool set_redraw = true;
|
||||
bool notify_ui = true;
|
||||
};
|
||||
|
||||
struct AppQueueDrainPlan {
|
||||
bool mark_running = true;
|
||||
bool drain_tasks = false;
|
||||
bool wrap_in_render_context = false;
|
||||
std::size_t task_count = 0;
|
||||
};
|
||||
|
||||
struct AppUiTickPlan {
|
||||
bool mark_running = true;
|
||||
bool execute_tasks = false;
|
||||
bool tick_app = true;
|
||||
bool update_before_render = false;
|
||||
bool enqueue_render_frame = false;
|
||||
std::size_t task_count = 0;
|
||||
};
|
||||
|
||||
struct AppUiLoopTimerPlan {
|
||||
bool update_platform_frame = true;
|
||||
float frame_accumulator = 0.0F;
|
||||
float fps_accumulator = 0.0F;
|
||||
float reload_accumulator = 0.0F;
|
||||
bool report_rendered_frames = false;
|
||||
int reported_frame_count = 0;
|
||||
int rendered_frames_after_report = 0;
|
||||
bool check_live_asset_reload = false;
|
||||
};
|
||||
|
||||
struct AppUiLoopRedrawPlan {
|
||||
bool tick_app = true;
|
||||
bool update_before_render = false;
|
||||
bool enqueue_render_frame = false;
|
||||
bool reset_frame_accumulator = false;
|
||||
int rendered_frames = 0;
|
||||
};
|
||||
|
||||
struct AppThreadStartPlan {
|
||||
bool start_thread = true;
|
||||
bool mark_running = true;
|
||||
};
|
||||
|
||||
struct AppThreadStopPlan {
|
||||
bool mark_not_running = true;
|
||||
bool notify_worker = true;
|
||||
bool join_thread = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr AppTaskDispatchPlan plan_app_task_dispatch(
|
||||
bool already_on_target_thread,
|
||||
bool unique,
|
||||
std::size_t queued_task_count,
|
||||
bool worker_running,
|
||||
bool wait_for_completion,
|
||||
bool request_redraw_after_dispatch) noexcept
|
||||
{
|
||||
const bool queue_task = !already_on_target_thread;
|
||||
return AppTaskDispatchPlan {
|
||||
.execute_immediately = already_on_target_thread,
|
||||
.queue_task = queue_task,
|
||||
.remove_matching_unique_task = queue_task && unique && queued_task_count > 0U,
|
||||
.notify_worker = queue_task,
|
||||
.wait_for_completion = queue_task && worker_running && wait_for_completion,
|
||||
.request_redraw = request_redraw_after_dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept
|
||||
{
|
||||
return AppAsyncRedrawPlan {};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_render_queue_drain(std::size_t queued_task_count) noexcept
|
||||
{
|
||||
const bool drain = queued_task_count > 0U;
|
||||
return AppQueueDrainPlan {
|
||||
.mark_running = true,
|
||||
.drain_tasks = drain,
|
||||
.wrap_in_render_context = drain,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppQueueDrainPlan plan_app_ui_queue_drain(std::size_t queued_task_count) noexcept
|
||||
{
|
||||
return AppQueueDrainPlan {
|
||||
.mark_running = true,
|
||||
.drain_tasks = queued_task_count > 0U,
|
||||
.wrap_in_render_context = false,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiTickPlan plan_app_ui_thread_tick(
|
||||
std::size_t queued_task_count,
|
||||
bool redraw) noexcept
|
||||
{
|
||||
return AppUiTickPlan {
|
||||
.mark_running = true,
|
||||
.execute_tasks = queued_task_count > 0U,
|
||||
.tick_app = true,
|
||||
.update_before_render = redraw,
|
||||
.enqueue_render_frame = redraw,
|
||||
.task_count = queued_task_count,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<AppUiLoopTimerPlan> plan_app_ui_loop_timers(
|
||||
float delta_time_seconds,
|
||||
float frame_accumulator,
|
||||
float fps_accumulator,
|
||||
float reload_accumulator,
|
||||
int rendered_frames,
|
||||
bool live_asset_reloading_enabled)
|
||||
{
|
||||
if (!std::isfinite(delta_time_seconds) || !std::isfinite(frame_accumulator)
|
||||
|| !std::isfinite(fps_accumulator) || !std::isfinite(reload_accumulator)) {
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI loop timer values must be finite"));
|
||||
}
|
||||
|
||||
if (delta_time_seconds < 0.0F || frame_accumulator < 0.0F
|
||||
|| fps_accumulator < 0.0F || reload_accumulator < 0.0F || rendered_frames < 0) {
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("UI loop timer values must not be negative"));
|
||||
}
|
||||
|
||||
AppUiLoopTimerPlan plan;
|
||||
plan.frame_accumulator = frame_accumulator + delta_time_seconds;
|
||||
plan.fps_accumulator = fps_accumulator + delta_time_seconds;
|
||||
plan.reload_accumulator = reload_accumulator;
|
||||
plan.rendered_frames_after_report = rendered_frames;
|
||||
|
||||
if (plan.fps_accumulator > 1.0F) {
|
||||
plan.report_rendered_frames = true;
|
||||
plan.reported_frame_count = rendered_frames;
|
||||
plan.fps_accumulator = 0.0F;
|
||||
plan.rendered_frames_after_report = 0;
|
||||
}
|
||||
|
||||
if (live_asset_reloading_enabled) {
|
||||
plan.reload_accumulator += delta_time_seconds;
|
||||
if (plan.reload_accumulator > 1.0F) {
|
||||
plan.reload_accumulator = 0.0F;
|
||||
plan.check_live_asset_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<AppUiLoopTimerPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppUiLoopRedrawPlan plan_app_ui_loop_redraw(
|
||||
bool redraw,
|
||||
int rendered_frames) noexcept
|
||||
{
|
||||
return AppUiLoopRedrawPlan {
|
||||
.tick_app = true,
|
||||
.update_before_render = redraw,
|
||||
.enqueue_render_frame = redraw,
|
||||
.reset_frame_accumulator = redraw,
|
||||
.rendered_frames = rendered_frames + (redraw ? 1 : 0),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppThreadStartPlan plan_app_thread_start() noexcept
|
||||
{
|
||||
return AppThreadStartPlan {};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr AppThreadStopPlan plan_app_thread_stop(bool thread_joinable) noexcept
|
||||
{
|
||||
return AppThreadStopPlan {
|
||||
.mark_not_running = true,
|
||||
.notify_worker = true,
|
||||
.join_thread = thread_joinable,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
71
src/app_core/brush_package_export.h
Normal file
71
src/app_core/brush_package_export.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct BrushPackageExportRequest {
|
||||
std::string author;
|
||||
std::string email;
|
||||
std::string url;
|
||||
std::string description;
|
||||
std::string destination_path;
|
||||
bool export_data = false;
|
||||
bool has_header_image = false;
|
||||
};
|
||||
|
||||
class BrushPackageExportServices {
|
||||
public:
|
||||
virtual ~BrushPackageExportServices() = default;
|
||||
|
||||
virtual void export_brush_package(std::string_view path, const BrushPackageExportRequest& request) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_export_path(std::string_view path) noexcept
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("brush package export path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_brush_package_export_success_dialog(std::string_view path)
|
||||
{
|
||||
std::string message = "Brushes exported to:\n";
|
||||
message += path;
|
||||
return plan_app_message_dialog("Export PPBR", message, false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_export_request(
|
||||
std::string_view path,
|
||||
const BrushPackageExportRequest& request) noexcept
|
||||
{
|
||||
(void)request;
|
||||
const auto path_status = validate_brush_package_export_path(path);
|
||||
if (!path_status.ok()) {
|
||||
return path_status;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_package_export(
|
||||
std::string_view path,
|
||||
const BrushPackageExportRequest& request,
|
||||
BrushPackageExportServices& services)
|
||||
{
|
||||
const auto status = validate_brush_package_export_request(path, request);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
services.export_brush_package(path, request);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
56
src/app_core/brush_package_import.h
Normal file
56
src/app_core/brush_package_import.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class BrushPackageImportKind {
|
||||
abr,
|
||||
ppbr,
|
||||
};
|
||||
|
||||
class BrushPackageImportServices {
|
||||
public:
|
||||
virtual ~BrushPackageImportServices() = default;
|
||||
|
||||
virtual void import_brush_package(BrushPackageImportKind kind, std::string_view path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline const char* brush_package_import_kind_name(BrushPackageImportKind kind) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case BrushPackageImportKind::abr:
|
||||
return "abr";
|
||||
case BrushPackageImportKind::ppbr:
|
||||
return "ppbr";
|
||||
}
|
||||
|
||||
return "abr";
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_brush_package_import_path(std::string_view path) noexcept
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("brush package import path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_brush_package_import(
|
||||
BrushPackageImportKind kind,
|
||||
std::string_view path,
|
||||
BrushPackageImportServices& services)
|
||||
{
|
||||
const auto status = validate_brush_package_import_path(path);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
services.import_brush_package(kind, path);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
1008
src/app_core/brush_ui.h
Normal file
1008
src/app_core/brush_ui.h
Normal file
File diff suppressed because it is too large
Load Diff
225
src/app_core/canvas_hotkey.h
Normal file
225
src/app_core/canvas_hotkey.h
Normal file
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/canvas_tool_ui.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasHotkeyEvent {
|
||||
key_down,
|
||||
key_up,
|
||||
touch_tap,
|
||||
};
|
||||
|
||||
enum class CanvasHotkeyKey {
|
||||
other,
|
||||
android_back,
|
||||
alt,
|
||||
e,
|
||||
s,
|
||||
tab,
|
||||
z,
|
||||
bracket_left,
|
||||
bracket_right,
|
||||
};
|
||||
|
||||
enum class CanvasHotkeyAction {
|
||||
none,
|
||||
select_tool,
|
||||
history,
|
||||
save_document,
|
||||
toggle_ui,
|
||||
adjust_brush_size,
|
||||
show_cursor,
|
||||
};
|
||||
|
||||
struct CanvasHotkeyState {
|
||||
bool ctrl_down = false;
|
||||
bool shift_down = false;
|
||||
bool mouse_focused = false;
|
||||
int undo_count = 0;
|
||||
int redo_count = 0;
|
||||
int touch_finger_count = 0;
|
||||
};
|
||||
|
||||
struct CanvasHotkeyPlan {
|
||||
CanvasHotkeyAction action = CanvasHotkeyAction::none;
|
||||
CanvasHotkeyEvent event = CanvasHotkeyEvent::key_up;
|
||||
CanvasHotkeyKey key = CanvasHotkeyKey::other;
|
||||
CanvasToolPlan tool;
|
||||
HistoryUiPlan history;
|
||||
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
|
||||
float brush_size_delta = 0.0F;
|
||||
bool no_op = true;
|
||||
};
|
||||
|
||||
class CanvasHotkeyServices {
|
||||
public:
|
||||
virtual ~CanvasHotkeyServices() = default;
|
||||
|
||||
virtual pp::foundation::Status execute_tool(const CanvasToolPlan& plan) = 0;
|
||||
virtual pp::foundation::Status execute_history(const HistoryUiPlan& plan) = 0;
|
||||
virtual void save_document(DocumentSaveIntent intent) = 0;
|
||||
virtual void toggle_ui() = 0;
|
||||
virtual void adjust_brush_size(float delta) = 0;
|
||||
virtual void show_cursor() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_canvas_hotkey_state(
|
||||
const CanvasHotkeyState& state) noexcept
|
||||
{
|
||||
if (state.undo_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("undo action count must not be negative");
|
||||
}
|
||||
if (state.redo_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("redo action count must not be negative");
|
||||
}
|
||||
if (state.touch_finger_count < 0) {
|
||||
return pp::foundation::Status::out_of_range("touch finger count must not be negative");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasHotkeyPlan> plan_canvas_hotkey(
|
||||
CanvasHotkeyEvent event,
|
||||
CanvasHotkeyKey key,
|
||||
const CanvasHotkeyState& state)
|
||||
{
|
||||
const auto state_status = validate_canvas_hotkey_state(state);
|
||||
if (!state_status.ok()) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(state_status);
|
||||
}
|
||||
|
||||
CanvasHotkeyPlan plan;
|
||||
plan.event = event;
|
||||
plan.key = key;
|
||||
|
||||
if (event == CanvasHotkeyEvent::touch_tap) {
|
||||
if (state.touch_finger_count == 2) {
|
||||
auto history = plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
}
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
if (event == CanvasHotkeyEvent::key_down) {
|
||||
switch (key) {
|
||||
case CanvasHotkeyKey::e:
|
||||
plan.action = CanvasHotkeyAction::select_tool;
|
||||
plan.tool = plan_canvas_tool_select(CanvasToolMode::erase);
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::android_back: {
|
||||
auto history = plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
break;
|
||||
}
|
||||
case CanvasHotkeyKey::alt:
|
||||
if (state.mouse_focused) {
|
||||
plan.action = CanvasHotkeyAction::show_cursor;
|
||||
plan.no_op = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case CanvasHotkeyKey::e:
|
||||
plan.action = CanvasHotkeyAction::select_tool;
|
||||
plan.tool = plan_canvas_tool_select(CanvasToolMode::draw);
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::tab:
|
||||
plan.action = CanvasHotkeyAction::toggle_ui;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::z:
|
||||
if (state.ctrl_down) {
|
||||
auto history = state.shift_down
|
||||
? plan_history_redo(state.redo_count)
|
||||
: plan_history_undo(state.undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = CanvasHotkeyAction::history;
|
||||
plan.history = history.value();
|
||||
plan.no_op = plan.history.no_op;
|
||||
}
|
||||
break;
|
||||
case CanvasHotkeyKey::s:
|
||||
if (state.ctrl_down) {
|
||||
plan.action = CanvasHotkeyAction::save_document;
|
||||
plan.save_intent = state.shift_down
|
||||
? DocumentSaveIntent::save_dirty_version
|
||||
: DocumentSaveIntent::save;
|
||||
plan.no_op = false;
|
||||
}
|
||||
break;
|
||||
case CanvasHotkeyKey::bracket_left:
|
||||
plan.action = CanvasHotkeyAction::adjust_brush_size;
|
||||
plan.brush_size_delta = -0.05F;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
case CanvasHotkeyKey::bracket_right:
|
||||
plan.action = CanvasHotkeyAction::adjust_brush_size;
|
||||
plan.brush_size_delta = 0.05F;
|
||||
plan.no_op = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasHotkeyPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_hotkey_plan(
|
||||
const CanvasHotkeyPlan& plan,
|
||||
CanvasHotkeyServices& services)
|
||||
{
|
||||
if (plan.no_op || plan.action == CanvasHotkeyAction::none) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
switch (plan.action) {
|
||||
case CanvasHotkeyAction::select_tool:
|
||||
return services.execute_tool(plan.tool);
|
||||
case CanvasHotkeyAction::history:
|
||||
return services.execute_history(plan.history);
|
||||
case CanvasHotkeyAction::save_document:
|
||||
services.save_document(plan.save_intent);
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::toggle_ui:
|
||||
services.toggle_ui();
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::adjust_brush_size:
|
||||
if (plan.brush_size_delta == 0.0F) {
|
||||
return pp::foundation::Status::invalid_argument("brush-size hotkey plan must include a delta");
|
||||
}
|
||||
services.adjust_brush_size(plan.brush_size_delta);
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::show_cursor:
|
||||
services.show_cursor();
|
||||
return pp::foundation::Status::success();
|
||||
case CanvasHotkeyAction::none:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown canvas hotkey action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
315
src/app_core/canvas_tool_ui.h
Normal file
315
src/app_core/canvas_tool_ui.h
Normal file
@@ -0,0 +1,315 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasToolOperation {
|
||||
select_mode,
|
||||
toggle_picking,
|
||||
toggle_touch_lock,
|
||||
};
|
||||
|
||||
enum class CanvasToolMode {
|
||||
draw,
|
||||
erase,
|
||||
line,
|
||||
camera,
|
||||
grid,
|
||||
copy,
|
||||
cut,
|
||||
fill,
|
||||
mask_free,
|
||||
mask_line,
|
||||
flood_fill,
|
||||
};
|
||||
|
||||
enum class CanvasToolTransformAction {
|
||||
none,
|
||||
copy,
|
||||
cut,
|
||||
};
|
||||
|
||||
enum class CanvasToolToolbarAction {
|
||||
select_mode,
|
||||
toggle_picking,
|
||||
toggle_touch_lock,
|
||||
};
|
||||
|
||||
enum class CanvasCursorVisibilityMode {
|
||||
never,
|
||||
small_brush,
|
||||
not_painting,
|
||||
always,
|
||||
};
|
||||
|
||||
struct CanvasToolPlan {
|
||||
CanvasToolOperation operation = CanvasToolOperation::select_mode;
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
CanvasToolTransformAction transform_action = CanvasToolTransformAction::none;
|
||||
bool selects_toolbar_button = false;
|
||||
bool updates_canvas_mode = false;
|
||||
bool toggles_picking = false;
|
||||
bool toggles_touch_lock = false;
|
||||
bool requires_draw_mode = false;
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
struct CanvasToolButtonState {
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
bool pick_active = false;
|
||||
bool touch_lock_active = false;
|
||||
bool pen_active = false;
|
||||
bool erase_active = false;
|
||||
bool line_active = false;
|
||||
bool camera_active = false;
|
||||
bool grid_active = false;
|
||||
bool copy_active = false;
|
||||
bool cut_active = false;
|
||||
bool fill_active = false;
|
||||
bool mask_free_active = false;
|
||||
bool mask_line_active = false;
|
||||
bool flood_fill_active = false;
|
||||
};
|
||||
|
||||
struct CanvasToolToolbarBinding {
|
||||
std::string_view button_id;
|
||||
CanvasToolToolbarAction action = CanvasToolToolbarAction::select_mode;
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
bool custom_button = true;
|
||||
bool applies_default_on_init = false;
|
||||
};
|
||||
|
||||
struct CanvasToolToolbarPlan {
|
||||
std::array<CanvasToolToolbarBinding, 13> bindings {};
|
||||
CanvasToolMode default_mode = CanvasToolMode::draw;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityInput {
|
||||
CanvasToolMode mode = CanvasToolMode::draw;
|
||||
CanvasCursorVisibilityMode visibility_mode = CanvasCursorVisibilityMode::never;
|
||||
bool has_current_brush = true;
|
||||
float brush_tip_size = 0.0F;
|
||||
bool pen_is_drawing = false;
|
||||
bool alt_down = false;
|
||||
bool pen_is_resizing = false;
|
||||
bool pen_is_picking = false;
|
||||
};
|
||||
|
||||
struct CanvasCursorVisibilityPlan {
|
||||
bool visible = true;
|
||||
bool paint_mode = false;
|
||||
bool uses_brush_size = false;
|
||||
bool uses_pen_state = false;
|
||||
bool forced_visible_by_modifier_or_tool = false;
|
||||
};
|
||||
|
||||
class CanvasToolServices {
|
||||
public:
|
||||
virtual ~CanvasToolServices() = default;
|
||||
|
||||
virtual void select_toolbar_button(CanvasToolMode mode) = 0;
|
||||
virtual void set_transform_action(CanvasToolTransformAction action) = 0;
|
||||
virtual void set_canvas_mode(CanvasToolMode mode) = 0;
|
||||
virtual void toggle_picking() = 0;
|
||||
virtual void toggle_touch_lock() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept
|
||||
{
|
||||
if (mode == CanvasToolMode::copy) {
|
||||
return CanvasToolTransformAction::copy;
|
||||
}
|
||||
if (mode == CanvasToolMode::cut) {
|
||||
return CanvasToolTransformAction::cut;
|
||||
}
|
||||
return CanvasToolTransformAction::none;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::select_mode;
|
||||
plan.mode = mode;
|
||||
plan.transform_action = transform_action_for_mode(mode);
|
||||
plan.selects_toolbar_button = true;
|
||||
plan.updates_canvas_mode = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::toggle_picking;
|
||||
plan.mode = CanvasToolMode::draw;
|
||||
plan.requires_draw_mode = true;
|
||||
plan.toggles_picking = current_mode_is_draw;
|
||||
plan.no_op = !current_mode_is_draw;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept
|
||||
{
|
||||
CanvasToolPlan plan;
|
||||
plan.operation = CanvasToolOperation::toggle_touch_lock;
|
||||
plan.toggles_touch_lock = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolToolbarPlan plan_canvas_tool_toolbar() noexcept
|
||||
{
|
||||
return {
|
||||
std::array<CanvasToolToolbarBinding, 13> {
|
||||
CanvasToolToolbarBinding { "btn-pen", CanvasToolToolbarAction::select_mode, CanvasToolMode::draw, true, true },
|
||||
CanvasToolToolbarBinding { "btn-pick", CanvasToolToolbarAction::toggle_picking, CanvasToolMode::draw, true, false },
|
||||
CanvasToolToolbarBinding { "btn-touchlock", CanvasToolToolbarAction::toggle_touch_lock, CanvasToolMode::draw, true, false },
|
||||
CanvasToolToolbarBinding { "btn-erase", CanvasToolToolbarAction::select_mode, CanvasToolMode::erase, true, false },
|
||||
CanvasToolToolbarBinding { "btn-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::line, true, false },
|
||||
CanvasToolToolbarBinding { "btn-cam", CanvasToolToolbarAction::select_mode, CanvasToolMode::camera, false, false },
|
||||
CanvasToolToolbarBinding { "btn-grid", CanvasToolToolbarAction::select_mode, CanvasToolMode::grid, false, false },
|
||||
CanvasToolToolbarBinding { "btn-copy", CanvasToolToolbarAction::select_mode, CanvasToolMode::copy, false, false },
|
||||
CanvasToolToolbarBinding { "btn-cut", CanvasToolToolbarAction::select_mode, CanvasToolMode::cut, false, false },
|
||||
CanvasToolToolbarBinding { "btn-fill", CanvasToolToolbarAction::select_mode, CanvasToolMode::fill, false, false },
|
||||
CanvasToolToolbarBinding { "btn-mask-free", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_free, true, false },
|
||||
CanvasToolToolbarBinding { "btn-mask-line", CanvasToolToolbarAction::select_mode, CanvasToolMode::mask_line, true, false },
|
||||
CanvasToolToolbarBinding { "btn-bucket", CanvasToolToolbarAction::select_mode, CanvasToolMode::flood_fill, true, false },
|
||||
},
|
||||
CanvasToolMode::draw,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_toolbar_binding_action(
|
||||
const CanvasToolToolbarBinding& binding,
|
||||
bool current_mode_is_draw) noexcept
|
||||
{
|
||||
switch (binding.action) {
|
||||
case CanvasToolToolbarAction::select_mode:
|
||||
return plan_canvas_tool_select(binding.mode);
|
||||
case CanvasToolToolbarAction::toggle_picking:
|
||||
return plan_canvas_tool_pick_toggle(current_mode_is_draw);
|
||||
case CanvasToolToolbarAction::toggle_touch_lock:
|
||||
return plan_canvas_tool_touch_lock_toggle();
|
||||
}
|
||||
|
||||
return plan_canvas_tool_select(CanvasToolMode::draw);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state(
|
||||
CanvasToolMode mode,
|
||||
bool picking,
|
||||
bool touch_lock) noexcept
|
||||
{
|
||||
CanvasToolButtonState state;
|
||||
state.mode = mode;
|
||||
state.pick_active = mode == CanvasToolMode::draw && picking;
|
||||
state.touch_lock_active = touch_lock;
|
||||
state.pen_active = mode == CanvasToolMode::draw;
|
||||
state.erase_active = mode == CanvasToolMode::erase;
|
||||
state.line_active = mode == CanvasToolMode::line;
|
||||
state.camera_active = mode == CanvasToolMode::camera;
|
||||
state.grid_active = mode == CanvasToolMode::grid;
|
||||
state.copy_active = mode == CanvasToolMode::copy;
|
||||
state.cut_active = mode == CanvasToolMode::cut;
|
||||
state.fill_active = mode == CanvasToolMode::fill;
|
||||
state.mask_free_active = mode == CanvasToolMode::mask_free;
|
||||
state.mask_line_active = mode == CanvasToolMode::mask_line;
|
||||
state.flood_fill_active = mode == CanvasToolMode::flood_fill;
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool canvas_tool_mode_is_paint(CanvasToolMode mode) noexcept
|
||||
{
|
||||
return mode == CanvasToolMode::draw || mode == CanvasToolMode::erase;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasCursorVisibilityPlan> plan_canvas_cursor_visibility(
|
||||
const CanvasCursorVisibilityInput& input)
|
||||
{
|
||||
CanvasCursorVisibilityPlan plan;
|
||||
plan.paint_mode = canvas_tool_mode_is_paint(input.mode);
|
||||
if (!plan.paint_mode) {
|
||||
plan.visible = true;
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
switch (input.visibility_mode) {
|
||||
case CanvasCursorVisibilityMode::always:
|
||||
plan.visible = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::never:
|
||||
plan.visible = false;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::small_brush:
|
||||
if (!input.has_current_brush) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor small-brush mode requires a current brush"));
|
||||
}
|
||||
if (!std::isfinite(input.brush_tip_size) || input.brush_tip_size < 0.0F) {
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas cursor brush size must be finite and non-negative"));
|
||||
}
|
||||
plan.visible = input.brush_tip_size < 10.0F;
|
||||
plan.uses_brush_size = true;
|
||||
break;
|
||||
case CanvasCursorVisibilityMode::not_painting:
|
||||
plan.visible = !input.pen_is_drawing;
|
||||
plan.uses_pen_state = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (input.alt_down || input.pen_is_resizing || input.pen_is_picking) {
|
||||
plan.visible = true;
|
||||
plan.forced_visible_by_modifier_or_tool = true;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasCursorVisibilityPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
|
||||
const CanvasToolPlan& plan,
|
||||
CanvasToolServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case CanvasToolOperation::select_mode:
|
||||
if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) {
|
||||
return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode");
|
||||
}
|
||||
if (plan.transform_action != transform_action_for_mode(plan.mode)) {
|
||||
return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action");
|
||||
}
|
||||
services.select_toolbar_button(plan.mode);
|
||||
if (plan.transform_action != CanvasToolTransformAction::none) {
|
||||
services.set_transform_action(plan.transform_action);
|
||||
}
|
||||
services.set_canvas_mode(plan.mode);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case CanvasToolOperation::toggle_picking:
|
||||
if (!plan.requires_draw_mode) {
|
||||
return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode");
|
||||
}
|
||||
if (plan.no_op) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
if (!plan.toggles_picking) {
|
||||
return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op");
|
||||
}
|
||||
services.toggle_picking();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case CanvasToolOperation::toggle_touch_lock:
|
||||
if (!plan.toggles_touch_lock || plan.no_op) {
|
||||
return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock");
|
||||
}
|
||||
services.toggle_touch_lock();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown canvas tool operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
115
src/app_core/canvas_view.h
Normal file
115
src/app_core/canvas_view.h
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CanvasViewCursorMode {
|
||||
never = 0,
|
||||
small_brush = 1,
|
||||
not_painting = 2,
|
||||
always = 3,
|
||||
};
|
||||
|
||||
struct CanvasCameraState {
|
||||
std::array<float, 16> rotation {};
|
||||
std::array<float, 3> position {};
|
||||
float field_of_view_degrees = 85.0F;
|
||||
std::array<float, 2> pan {};
|
||||
};
|
||||
|
||||
struct CanvasViewDensityPlan {
|
||||
float density = 1.0F;
|
||||
bool recreates_buffers = true;
|
||||
};
|
||||
|
||||
struct CanvasViewCursorModePlan {
|
||||
CanvasViewCursorMode mode = CanvasViewCursorMode::never;
|
||||
};
|
||||
|
||||
class CanvasViewServices {
|
||||
public:
|
||||
virtual ~CanvasViewServices() = default;
|
||||
|
||||
virtual void reset_camera(const CanvasCameraState& state) = 0;
|
||||
virtual void set_density(const CanvasViewDensityPlan& plan) = 0;
|
||||
virtual void set_cursor_mode(const CanvasViewCursorModePlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr CanvasCameraState plan_canvas_camera_reset() noexcept
|
||||
{
|
||||
CanvasCameraState state;
|
||||
state.rotation = {
|
||||
1.0F, 0.0F, 0.0F, 0.0F,
|
||||
0.0F, 1.0F, 0.0F, 0.0F,
|
||||
0.0F, 0.0F, 1.0F, 0.0F,
|
||||
0.0F, 0.0F, 0.0F, 1.0F,
|
||||
};
|
||||
state.position = { 0.0F, 0.0F, 0.0F };
|
||||
state.field_of_view_degrees = 85.0F;
|
||||
state.pan = { 0.0F, 0.0F };
|
||||
return state;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasViewDensityPlan> plan_canvas_view_density(float density)
|
||||
{
|
||||
if (!std::isfinite(density) || density <= 0.0F) {
|
||||
return pp::foundation::Result<CanvasViewDensityPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("canvas view density must be finite and positive"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasViewDensityPlan>::success(CanvasViewDensityPlan {
|
||||
.density = density,
|
||||
.recreates_buffers = true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CanvasViewCursorModePlan> plan_canvas_view_cursor_mode(int mode)
|
||||
{
|
||||
if (mode < static_cast<int>(CanvasViewCursorMode::never)
|
||||
|| mode > static_cast<int>(CanvasViewCursorMode::always)) {
|
||||
return pp::foundation::Result<CanvasViewCursorModePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("canvas cursor mode is out of range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasViewCursorModePlan>::success(CanvasViewCursorModePlan {
|
||||
.mode = static_cast<CanvasViewCursorMode>(mode),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_camera_reset(CanvasViewServices& services)
|
||||
{
|
||||
services.reset_camera(plan_canvas_camera_reset());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_view_density(
|
||||
float density,
|
||||
CanvasViewServices& services)
|
||||
{
|
||||
const auto plan = plan_canvas_view_density(density);
|
||||
if (!plan) {
|
||||
return plan.status();
|
||||
}
|
||||
|
||||
services.set_density(plan.value());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_canvas_view_cursor_mode(
|
||||
int mode,
|
||||
CanvasViewServices& services)
|
||||
{
|
||||
const auto plan = plan_canvas_view_cursor_mode(mode);
|
||||
if (!plan) {
|
||||
return plan.status();
|
||||
}
|
||||
|
||||
services.set_cursor_mode(plan.value());
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
97
src/app_core/command_convert.h
Normal file
97
src/app_core/command_convert.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CommandConvertStep {
|
||||
apply_renderer_state,
|
||||
create_canvas,
|
||||
open_project,
|
||||
export_equirectangular,
|
||||
};
|
||||
|
||||
struct CommandConvertPlan {
|
||||
std::string project_path;
|
||||
std::string output_path;
|
||||
int canvas_resolution = 0;
|
||||
std::vector<CommandConvertStep> steps;
|
||||
};
|
||||
|
||||
class CommandConvertServices {
|
||||
public:
|
||||
virtual ~CommandConvertServices() = default;
|
||||
|
||||
virtual void apply_renderer_state() = 0;
|
||||
virtual void create_canvas(int canvas_resolution) = 0;
|
||||
virtual void open_project(std::string_view project_path) = 0;
|
||||
virtual void export_equirectangular(std::string_view output_path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<CommandConvertPlan> plan_command_convert(
|
||||
std::string_view project_path,
|
||||
std::string_view output_path,
|
||||
int canvas_resolution)
|
||||
{
|
||||
if (project_path.empty()) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert project path must not be empty"));
|
||||
}
|
||||
|
||||
if (output_path.empty()) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert output path must not be empty"));
|
||||
}
|
||||
|
||||
if (canvas_resolution < 1) {
|
||||
return pp::foundation::Result<CommandConvertPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("convert canvas resolution must be positive"));
|
||||
}
|
||||
|
||||
CommandConvertPlan plan;
|
||||
plan.project_path = std::string(project_path);
|
||||
plan.output_path = std::string(output_path);
|
||||
plan.canvas_resolution = canvas_resolution;
|
||||
plan.steps = {
|
||||
CommandConvertStep::apply_renderer_state,
|
||||
CommandConvertStep::create_canvas,
|
||||
CommandConvertStep::open_project,
|
||||
CommandConvertStep::export_equirectangular,
|
||||
};
|
||||
return pp::foundation::Result<CommandConvertPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_command_convert_plan(
|
||||
const CommandConvertPlan& plan,
|
||||
CommandConvertServices& services)
|
||||
{
|
||||
if (plan.project_path.empty() || plan.output_path.empty() || plan.canvas_resolution < 1) {
|
||||
return pp::foundation::Status::invalid_argument("convert plan is malformed");
|
||||
}
|
||||
|
||||
for (const auto step : plan.steps) {
|
||||
switch (step) {
|
||||
case CommandConvertStep::apply_renderer_state:
|
||||
services.apply_renderer_state();
|
||||
break;
|
||||
case CommandConvertStep::create_canvas:
|
||||
services.create_canvas(plan.canvas_resolution);
|
||||
break;
|
||||
case CommandConvertStep::open_project:
|
||||
services.open_project(plan.project_path);
|
||||
break;
|
||||
case CommandConvertStep::export_equirectangular:
|
||||
services.export_equirectangular(plan.output_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
836
src/app_core/document_animation.h
Normal file
836
src/app_core/document_animation.h
Normal file
@@ -0,0 +1,836 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr int document_animation_default_frame_duration = 1;
|
||||
|
||||
enum class DocumentAnimationOperation {
|
||||
add_frame,
|
||||
duplicate_frame,
|
||||
remove_frame,
|
||||
adjust_duration,
|
||||
move_frame,
|
||||
select_frame,
|
||||
goto_frame,
|
||||
goto_next,
|
||||
goto_previous,
|
||||
playback_step,
|
||||
toggle_playback,
|
||||
set_onion_size,
|
||||
};
|
||||
|
||||
enum class DocumentAnimationPanelAction {
|
||||
goto_frame,
|
||||
next_frame,
|
||||
previous_frame,
|
||||
playback_step,
|
||||
toggle_playback,
|
||||
};
|
||||
|
||||
struct DocumentAnimationPanelState {
|
||||
int total_duration = 1;
|
||||
int current_frame = 0;
|
||||
bool playback_active = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationOperationPlan {
|
||||
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int selected_frame = 0;
|
||||
int target_frame = 0;
|
||||
int frame_duration = document_animation_default_frame_duration;
|
||||
int duration_delta = 0;
|
||||
int move_offset = 0;
|
||||
int onion_size = 1;
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
int playback_idle_ms = 100;
|
||||
bool requires_selected_frame = false;
|
||||
bool mutates_document = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_canvas_animation = false;
|
||||
bool marks_unsaved = false;
|
||||
bool playback_was_active = false;
|
||||
bool playback_active = false;
|
||||
bool resets_playback_timer = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationOnionFrameRange {
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int onion_size = 0;
|
||||
int first_frame = 0;
|
||||
int last_frame = 0;
|
||||
};
|
||||
|
||||
inline constexpr float document_animation_timeline_frame_width = 35.0F;
|
||||
|
||||
struct DocumentAnimationTimelineScrubPlan {
|
||||
int total_duration = 1;
|
||||
float cursor_x = 0.0F;
|
||||
float frame_width = document_animation_timeline_frame_width;
|
||||
int target_frame = 0;
|
||||
};
|
||||
|
||||
struct DocumentAnimationLayerInput {
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
std::vector<int> frame_durations;
|
||||
};
|
||||
|
||||
struct DocumentAnimationFrameView {
|
||||
int frame_index = 0;
|
||||
int duration = document_animation_default_frame_duration;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
struct DocumentAnimationLayerView {
|
||||
int layer_index = 0;
|
||||
std::uint32_t layer_id = 0;
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
bool current = false;
|
||||
std::vector<DocumentAnimationFrameView> frames;
|
||||
};
|
||||
|
||||
struct DocumentAnimationPanelView {
|
||||
int total_duration = 1;
|
||||
int current_frame = 0;
|
||||
int onion_size = 0;
|
||||
std::uint32_t selected_layer_id = 0;
|
||||
int selected_frame = -1;
|
||||
bool has_selected_frame = false;
|
||||
std::vector<DocumentAnimationLayerView> layers;
|
||||
};
|
||||
|
||||
class DocumentAnimationServices {
|
||||
public:
|
||||
virtual ~DocumentAnimationServices() = default;
|
||||
|
||||
virtual void add_frame() = 0;
|
||||
virtual void duplicate_frame(int selected_frame) = 0;
|
||||
virtual void remove_frame(int selected_frame, int target_frame) = 0;
|
||||
virtual void set_frame_duration(int selected_frame, int duration) = 0;
|
||||
virtual int move_frame(int selected_frame, int move_offset) = 0;
|
||||
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
|
||||
virtual void select_layer(int layer_index) = 0;
|
||||
virtual void goto_frame(int target_frame) = 0;
|
||||
virtual void set_timeline_frame(int target_frame) = 0;
|
||||
virtual void set_onion_size(int onion_size) = 0;
|
||||
virtual void capture_playback_restore_mode() = 0;
|
||||
virtual void enter_playback_camera_mode() = 0;
|
||||
virtual void restore_playback_canvas_mode() = 0;
|
||||
virtual void set_playback_active(bool active) = 0;
|
||||
virtual void reset_playback_timer() = 0;
|
||||
virtual void set_playback_idle_ms(int idle_ms) = 0;
|
||||
virtual void update_canvas_animation() = 0;
|
||||
virtual void update_frame_status() = 0;
|
||||
virtual void reload_animation_layers() = 0;
|
||||
virtual void mark_unsaved() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
|
||||
{
|
||||
if (frame_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
|
||||
int frame_count,
|
||||
int index) noexcept
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= frame_count) {
|
||||
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
|
||||
{
|
||||
if (duration < 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationPanelView> plan_animation_panel_view(
|
||||
const std::vector<DocumentAnimationLayerInput>& layers,
|
||||
int total_duration,
|
||||
int current_layer_index,
|
||||
int current_frame,
|
||||
std::uint32_t selected_layer_id,
|
||||
int selected_frame,
|
||||
int onion_size)
|
||||
{
|
||||
if (layers.empty()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation panel requires at least one layer"));
|
||||
}
|
||||
|
||||
const auto timeline_status = validate_animation_frame_index(total_duration, current_frame);
|
||||
if (!timeline_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(timeline_status);
|
||||
}
|
||||
|
||||
if (current_layer_index < 0 || current_layer_index >= static_cast<int>(layers.size())) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation layer index is outside the document"));
|
||||
}
|
||||
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationPanelView view;
|
||||
view.total_duration = total_duration;
|
||||
view.current_frame = current_frame;
|
||||
view.onion_size = onion_size;
|
||||
view.selected_layer_id = selected_layer_id;
|
||||
view.selected_frame = selected_frame;
|
||||
view.layers.reserve(layers.size());
|
||||
|
||||
for (std::size_t i = 0; i < layers.size(); ++i) {
|
||||
const auto& input = layers[i];
|
||||
if (input.layer_index < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||
}
|
||||
if (input.frame_durations.empty()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must contain at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationLayerView layer;
|
||||
layer.layer_index = input.layer_index;
|
||||
layer.layer_id = input.layer_id;
|
||||
layer.name = input.name;
|
||||
layer.visible = input.visible;
|
||||
layer.current = input.layer_index == current_layer_index;
|
||||
layer.frames.reserve(input.frame_durations.size());
|
||||
|
||||
for (std::size_t frame_index = 0; frame_index < input.frame_durations.size(); ++frame_index) {
|
||||
const int duration = input.frame_durations[frame_index];
|
||||
const auto duration_status = validate_animation_frame_duration(duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::failure(duration_status);
|
||||
}
|
||||
|
||||
const bool selected = selected_frame >= 0
|
||||
&& input.layer_id == selected_layer_id
|
||||
&& static_cast<int>(frame_index) == selected_frame;
|
||||
view.has_selected_frame = view.has_selected_frame || selected;
|
||||
layer.frames.push_back(DocumentAnimationFrameView {
|
||||
.frame_index = static_cast<int>(frame_index),
|
||||
.duration = duration,
|
||||
.selected = selected,
|
||||
});
|
||||
}
|
||||
|
||||
view.layers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationPanelView>::success(std::move(view));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
|
||||
int frame_count,
|
||||
int current_frame,
|
||||
int onion_size)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, current_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(index_status);
|
||||
}
|
||||
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
const auto first = std::max<std::int64_t>(
|
||||
static_cast<std::int64_t>(current_frame) - static_cast<std::int64_t>(onion_size),
|
||||
0);
|
||||
const auto last = std::min<std::int64_t>(
|
||||
static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(onion_size),
|
||||
static_cast<std::int64_t>(frame_count) - 1);
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationOnionFrameRange>::success(
|
||||
DocumentAnimationOnionFrameRange {
|
||||
.frame_count = frame_count,
|
||||
.current_frame = current_frame,
|
||||
.onion_size = onion_size,
|
||||
.first_frame = static_cast<int>(first),
|
||||
.last_frame = static_cast<int>(last),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationTimelineScrubPlan> plan_animation_timeline_scrub(
|
||||
int total_duration,
|
||||
float cursor_x,
|
||||
float frame_width = document_animation_timeline_frame_width)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (!std::isfinite(cursor_x)) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline cursor position must be finite"));
|
||||
}
|
||||
|
||||
if (!std::isfinite(frame_width) || frame_width <= 0.0F) {
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation timeline frame width must be positive and finite"));
|
||||
}
|
||||
|
||||
const auto raw_frame = static_cast<std::int64_t>(std::floor(cursor_x / frame_width));
|
||||
const auto target_frame = std::clamp<std::int64_t>(raw_frame, 0, total_duration - 1);
|
||||
return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::success(
|
||||
DocumentAnimationTimelineScrubPlan {
|
||||
.total_duration = total_duration,
|
||||
.cursor_x = cursor_x,
|
||||
.frame_width = frame_width,
|
||||
.target_frame = static_cast<int>(target_frame),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline float animation_onion_frame_alpha(
|
||||
const DocumentAnimationOnionFrameRange& range,
|
||||
int frame) noexcept
|
||||
{
|
||||
if (frame < range.first_frame || frame > range.last_frame) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const int distance = frame >= range.current_frame
|
||||
? frame - range.current_frame
|
||||
: range.current_frame - frame;
|
||||
if (distance > range.onion_size) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 1.0f - static_cast<float>(distance) / static_cast<float>(range.onion_size + 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
|
||||
int frame_count,
|
||||
int current_frame)
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
if (current_frame < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::add_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.current_frame = current_frame;
|
||||
plan.selected_frame = frame_count;
|
||||
plan.target_frame = current_frame;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::duplicate_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame + 1;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (frame_count <= 1) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::remove_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = std::min(selected_frame, frame_count - 2);
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int current_duration,
|
||||
int delta)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto duration_status = validate_animation_frame_duration(current_duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
|
||||
}
|
||||
|
||||
if (delta == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
|
||||
}
|
||||
|
||||
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::adjust_duration;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.frame_duration = std::max(current_duration + delta, 1);
|
||||
plan.duration_delta = delta;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.frame_duration != current_duration;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_canvas_animation = plan.mutates_document;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::move_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
|
||||
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
|
||||
plan.move_offset = offset;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.target_frame != selected_frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
|
||||
int frame_count,
|
||||
int layer_index,
|
||||
std::uint32_t layer_id,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (layer_index < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::select_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.layer_index = layer_index;
|
||||
plan.layer_id = layer_id;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
|
||||
int total_duration,
|
||||
int frame)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (frame < 0 || frame >= total_duration) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::goto_frame;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = frame;
|
||||
plan.target_frame = frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
|
||||
if (!current_status) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = current_frame;
|
||||
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
|
||||
% static_cast<std::int64_t>(total_duration);
|
||||
if (target < 0) {
|
||||
target += total_duration;
|
||||
}
|
||||
plan.target_frame = static_cast<int>(target);
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
|
||||
if (!step) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
|
||||
}
|
||||
|
||||
auto plan = step.value();
|
||||
plan.operation = DocumentAnimationOperation::playback_step;
|
||||
plan.move_offset = offset;
|
||||
plan.reloads_animation_layers = false;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
|
||||
bool playback_active)
|
||||
{
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::toggle_playback;
|
||||
plan.playback_was_active = playback_active;
|
||||
plan.playback_active = !playback_active;
|
||||
plan.playback_idle_ms = playback_active ? 100 : 10;
|
||||
plan.resets_playback_timer = !playback_active;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
|
||||
{
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::set_onion_size;
|
||||
plan.onion_size = onion_size;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
|
||||
DocumentAnimationPanelAction action,
|
||||
const DocumentAnimationPanelState& state,
|
||||
int target_frame = 0)
|
||||
{
|
||||
switch (action) {
|
||||
case DocumentAnimationPanelAction::goto_frame:
|
||||
return plan_animation_goto_frame(state.total_duration, target_frame);
|
||||
|
||||
case DocumentAnimationPanelAction::next_frame:
|
||||
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
|
||||
|
||||
case DocumentAnimationPanelAction::previous_frame:
|
||||
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
|
||||
|
||||
case DocumentAnimationPanelAction::playback_step:
|
||||
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
|
||||
|
||||
case DocumentAnimationPanelAction::toggle_playback:
|
||||
return plan_animation_playback_toggle(state.playback_active);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown animation panel action"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
|
||||
const DocumentAnimationOperationPlan& plan) noexcept
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentAnimationOperation::add_frame:
|
||||
if (!plan.mutates_document || !plan.marks_unsaved) {
|
||||
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
|
||||
}
|
||||
return validate_animation_frame_count(plan.frame_count);
|
||||
|
||||
case DocumentAnimationOperation::duplicate_frame:
|
||||
case DocumentAnimationOperation::remove_frame:
|
||||
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
|
||||
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
|
||||
}
|
||||
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
|
||||
case DocumentAnimationOperation::adjust_duration:
|
||||
if (!plan.requires_selected_frame) {
|
||||
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
|
||||
}
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
}
|
||||
return validate_animation_frame_duration(plan.frame_duration);
|
||||
|
||||
case DocumentAnimationOperation::move_frame:
|
||||
if (!plan.requires_selected_frame || plan.move_offset == 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
|
||||
case DocumentAnimationOperation::select_frame:
|
||||
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
|
||||
}
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||
|
||||
case DocumentAnimationOperation::goto_frame:
|
||||
case DocumentAnimationOperation::goto_next:
|
||||
case DocumentAnimationOperation::goto_previous:
|
||||
case DocumentAnimationOperation::playback_step:
|
||||
if (!plan.updates_canvas_animation) {
|
||||
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
|
||||
}
|
||||
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
|
||||
}
|
||||
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
|
||||
|
||||
case DocumentAnimationOperation::toggle_playback:
|
||||
if (plan.playback_active == plan.playback_was_active) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
|
||||
}
|
||||
if (plan.playback_idle_ms <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
|
||||
}
|
||||
if (plan.playback_active && !plan.resets_playback_timer) {
|
||||
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::set_onion_size:
|
||||
if (plan.onion_size < 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
|
||||
}
|
||||
if (!plan.updates_canvas_animation) {
|
||||
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
|
||||
const DocumentAnimationOperationPlan& plan,
|
||||
DocumentAnimationServices& services)
|
||||
{
|
||||
const auto validation = validate_animation_operation_plan(plan);
|
||||
if (!validation.ok()) {
|
||||
return validation;
|
||||
}
|
||||
|
||||
switch (plan.operation) {
|
||||
case DocumentAnimationOperation::add_frame:
|
||||
services.add_frame();
|
||||
services.mark_unsaved();
|
||||
services.update_canvas_animation();
|
||||
services.reload_animation_layers();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::duplicate_frame:
|
||||
services.duplicate_frame(plan.selected_frame);
|
||||
services.mark_unsaved();
|
||||
services.update_canvas_animation();
|
||||
services.reload_animation_layers();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::remove_frame:
|
||||
services.remove_frame(plan.selected_frame, plan.target_frame);
|
||||
services.mark_unsaved();
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(plan.target_frame);
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::adjust_duration:
|
||||
if (plan.mutates_document) {
|
||||
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
|
||||
services.mark_unsaved();
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.update_canvas_animation();
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::move_frame:
|
||||
{
|
||||
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
|
||||
if (plan.marks_unsaved) {
|
||||
services.mark_unsaved();
|
||||
}
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(actual_target_frame);
|
||||
}
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
case DocumentAnimationOperation::goto_frame:
|
||||
case DocumentAnimationOperation::goto_next:
|
||||
case DocumentAnimationOperation::goto_previous:
|
||||
services.goto_frame(plan.target_frame);
|
||||
if (plan.reloads_animation_layers) {
|
||||
services.reload_animation_layers();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::select_frame:
|
||||
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.goto_frame(plan.target_frame);
|
||||
}
|
||||
services.select_layer(plan.layer_index);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::playback_step:
|
||||
services.goto_frame(plan.target_frame);
|
||||
services.set_timeline_frame(plan.target_frame);
|
||||
services.update_frame_status();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::toggle_playback:
|
||||
if (plan.playback_active) {
|
||||
services.capture_playback_restore_mode();
|
||||
services.enter_playback_camera_mode();
|
||||
if (plan.resets_playback_timer) {
|
||||
services.reset_playback_timer();
|
||||
}
|
||||
} else {
|
||||
services.restore_playback_canvas_mode();
|
||||
}
|
||||
services.set_playback_active(plan.playback_active);
|
||||
services.set_playback_idle_ms(plan.playback_idle_ms);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case DocumentAnimationOperation::set_onion_size:
|
||||
services.set_onion_size(plan.onion_size);
|
||||
if (plan.updates_canvas_animation) {
|
||||
services.update_canvas_animation();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown animation operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
526
src/app_core/document_canvas.h
Normal file
526
src/app_core/document_canvas.h
Normal file
@@ -0,0 +1,526 @@
|
||||
#pragma once
|
||||
|
||||
#include "document/document.h"
|
||||
#include "document/ppi_export.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct DocumentCanvasClearPlan {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
float b = 0.0F;
|
||||
float a = 0.0F;
|
||||
bool clears_canvas = false;
|
||||
bool records_undo = false;
|
||||
bool marks_unsaved = false;
|
||||
bool no_op = true;
|
||||
};
|
||||
|
||||
struct DocumentCanvasFacePayloadInput {
|
||||
std::uint32_t frame_index = 0;
|
||||
std::uint32_t face_index = 0;
|
||||
std::uint32_t x = 0;
|
||||
std::uint32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::span<const std::uint8_t> rgba8;
|
||||
};
|
||||
|
||||
struct DocumentCanvasLayerSnapshotInput {
|
||||
std::string_view name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
int blend_mode = 0;
|
||||
std::span<const std::uint32_t> frame_durations_ms;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
std::span<const DocumentCanvasFacePayloadInput> captured_face_payloads;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSnapshotInput {
|
||||
bool has_canvas = true;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::size_t active_layer_index = 0;
|
||||
std::size_t active_frame_index = 0;
|
||||
std::span<const DocumentCanvasLayerSnapshotInput> layers;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSnapshotResult {
|
||||
pp::document::CanvasDocument document;
|
||||
std::size_t layer_count = 0;
|
||||
std::size_t frame_count = 0;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
std::size_t captured_face_payloads = 0;
|
||||
bool metadata_only = false;
|
||||
bool requires_renderer_payload_readback = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasSaveSnapshotReport {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::size_t layer_count = 0;
|
||||
std::size_t frame_count = 0;
|
||||
std::size_t captured_face_payloads = 0;
|
||||
std::size_t pending_face_payloads = 0;
|
||||
bool payload_complete = false;
|
||||
bool can_export_ppi = false;
|
||||
};
|
||||
|
||||
enum class DocumentCanvasSaveWriterAction {
|
||||
use_document_ppi_writer,
|
||||
use_legacy_project_save,
|
||||
};
|
||||
|
||||
struct DocumentCanvasSaveWriterRoutePlan {
|
||||
DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save;
|
||||
bool payload_complete = false;
|
||||
bool can_export_ppi = false;
|
||||
bool uses_document_ppi_writer = false;
|
||||
std::string_view fallback_reason;
|
||||
};
|
||||
|
||||
struct DocumentCanvasPpiExportResult {
|
||||
DocumentCanvasSaveSnapshotReport report;
|
||||
std::vector<std::byte> bytes;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveTargetPlan {
|
||||
std::string target_path;
|
||||
std::string file_name;
|
||||
std::string temporary_path;
|
||||
std::string timelapse_path;
|
||||
};
|
||||
|
||||
enum class DocumentCanvasProjectSaveWriteAction {
|
||||
write_direct_to_target,
|
||||
write_temporary_then_swap,
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveWritePlan {
|
||||
DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target;
|
||||
std::string write_path;
|
||||
std::string target_path;
|
||||
std::string temporary_path;
|
||||
bool target_exists = false;
|
||||
bool uses_temporary = false;
|
||||
bool falls_back_to_direct_on_temporary_open_failure = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveCommitInput {
|
||||
bool used_temporary = false;
|
||||
bool target_remove_attempted = false;
|
||||
bool target_remove_succeeded = false;
|
||||
bool temporary_rename_attempted = false;
|
||||
bool temporary_rename_succeeded = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSaveCommitPlan {
|
||||
bool saved = false;
|
||||
bool used_temporary = false;
|
||||
bool target_removed = false;
|
||||
bool temporary_renamed = false;
|
||||
bool target_may_be_missing = false;
|
||||
std::string_view log_message;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSavePostCommitInput {
|
||||
bool save_succeeded = false;
|
||||
bool timelapse_encoder_available = false;
|
||||
bool progress_ui_visible = false;
|
||||
};
|
||||
|
||||
struct DocumentCanvasProjectSavePostCommitPlan {
|
||||
bool marks_document_clean = false;
|
||||
bool marks_new_document_committed = false;
|
||||
bool saves_timelapse_sidecar = false;
|
||||
bool flushes_platform_storage = false;
|
||||
bool dismisses_progress_ui = false;
|
||||
bool updates_title = true;
|
||||
};
|
||||
|
||||
class DocumentCanvasClearServices {
|
||||
public:
|
||||
virtual ~DocumentCanvasClearServices() = default;
|
||||
|
||||
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
|
||||
}
|
||||
if (value < 0.0F || value > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
|
||||
DocumentCanvasSnapshotInput input)
|
||||
{
|
||||
if (!input.has_canvas) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
|
||||
}
|
||||
|
||||
if (input.layers.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
|
||||
}
|
||||
|
||||
std::size_t frame_count = 1U;
|
||||
std::size_t pending_face_payloads = 0U;
|
||||
std::size_t captured_face_payloads = 0U;
|
||||
for (const auto& layer : input.layers) {
|
||||
frame_count = std::max(frame_count, layer.frame_durations_ms.size());
|
||||
pending_face_payloads += layer.pending_face_payloads;
|
||||
captured_face_payloads += layer.captured_face_payloads.size();
|
||||
}
|
||||
|
||||
if (input.active_layer_index >= input.layers.size()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
|
||||
}
|
||||
|
||||
if (input.active_frame_index >= frame_count) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
|
||||
pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
|
||||
}
|
||||
|
||||
std::vector<pp::document::AnimationFrame> root_frames;
|
||||
root_frames.reserve(frame_count);
|
||||
for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
|
||||
std::uint32_t duration_ms = 100U;
|
||||
for (const auto& layer : input.layers) {
|
||||
if (frame_index < layer.frame_durations_ms.size()) {
|
||||
duration_ms = layer.frame_durations_ms[frame_index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||
}
|
||||
|
||||
std::vector<std::string> layer_names;
|
||||
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
|
||||
std::vector<pp::document::DocumentLayerConfig> layer_configs;
|
||||
layer_names.reserve(input.layers.size());
|
||||
layer_frames.reserve(input.layers.size());
|
||||
layer_configs.reserve(input.layers.size());
|
||||
|
||||
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
||||
const auto& layer = input.layers[layer_index];
|
||||
if (layer.name.empty()) {
|
||||
layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
|
||||
} else {
|
||||
layer_names.push_back(std::string(layer.name));
|
||||
}
|
||||
|
||||
layer_frames.push_back({});
|
||||
auto& frames = layer_frames.back();
|
||||
frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
|
||||
if (layer.frame_durations_ms.empty()) {
|
||||
frames = root_frames;
|
||||
} else {
|
||||
for (const auto duration_ms : layer.frame_durations_ms) {
|
||||
frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
|
||||
}
|
||||
}
|
||||
|
||||
layer_configs.push_back(pp::document::DocumentLayerConfig {
|
||||
.name = layer_names.back(),
|
||||
.visible = layer.visible,
|
||||
.alpha_locked = layer.alpha_locked,
|
||||
.opacity = layer.opacity,
|
||||
.blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
|
||||
.frames = std::span<const pp::document::AnimationFrame>(frames),
|
||||
});
|
||||
}
|
||||
|
||||
auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
|
||||
.width = input.width,
|
||||
.height = input.height,
|
||||
.layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
|
||||
.frames = std::span<const pp::document::AnimationFrame>(root_frames),
|
||||
});
|
||||
if (!document) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
|
||||
}
|
||||
|
||||
for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
|
||||
for (const auto& payload : input.layers[layer_index].captured_face_payloads) {
|
||||
pp::document::LayerFacePixels pixels {
|
||||
.face_index = payload.face_index,
|
||||
.x = payload.x,
|
||||
.y = payload.y,
|
||||
.width = payload.width,
|
||||
.height = payload.height,
|
||||
.rgba8 = std::vector<std::uint8_t>(payload.rgba8.begin(), payload.rgba8.end()),
|
||||
};
|
||||
const auto payload_status = document.value().set_layer_frame_face_pixels(
|
||||
layer_index,
|
||||
payload.frame_index,
|
||||
std::move(pixels));
|
||||
if (!payload_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(payload_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto active_status = document.value().set_active_layer(input.active_layer_index);
|
||||
if (!active_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||
}
|
||||
|
||||
active_status = document.value().set_active_frame(input.active_frame_index);
|
||||
if (!active_status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
|
||||
.document = std::move(document.value()),
|
||||
.layer_count = input.layers.size(),
|
||||
.frame_count = frame_count,
|
||||
.pending_face_payloads = pending_face_payloads,
|
||||
.captured_face_payloads = captured_face_payloads,
|
||||
.metadata_only = captured_face_payloads == 0U,
|
||||
.requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report(
|
||||
const DocumentCanvasSnapshotResult& snapshot) noexcept
|
||||
{
|
||||
return DocumentCanvasSaveSnapshotReport {
|
||||
.width = snapshot.document.width(),
|
||||
.height = snapshot.document.height(),
|
||||
.layer_count = snapshot.layer_count,
|
||||
.frame_count = snapshot.frame_count,
|
||||
.captured_face_payloads = snapshot.captured_face_payloads,
|
||||
.pending_face_payloads = snapshot.pending_face_payloads,
|
||||
.payload_complete = !snapshot.requires_renderer_payload_readback,
|
||||
.can_export_ppi = !snapshot.requires_renderer_payload_readback,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route(
|
||||
DocumentCanvasSaveSnapshotReport report) noexcept
|
||||
{
|
||||
DocumentCanvasSaveWriterRoutePlan plan;
|
||||
plan.payload_complete = report.payload_complete;
|
||||
plan.can_export_ppi = report.can_export_ppi;
|
||||
|
||||
if (!report.payload_complete || !report.can_export_ppi) {
|
||||
plan.fallback_reason = "canvas document snapshot still requires renderer payload readback";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer;
|
||||
plan.uses_document_ppi_writer = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasPpiExportResult>
|
||||
export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot)
|
||||
{
|
||||
const auto report = make_document_canvas_save_snapshot_report(snapshot);
|
||||
const auto route = plan_document_canvas_save_writer_route(report);
|
||||
if (!route.uses_document_ppi_writer) {
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(
|
||||
pp::foundation::Status::invalid_argument(route.fallback_reason.data()));
|
||||
}
|
||||
|
||||
auto bytes = pp::document::export_ppi_project_document(snapshot.document);
|
||||
if (!bytes) {
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(bytes.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentCanvasPpiExportResult>::success(DocumentCanvasPpiExportResult {
|
||||
.report = report,
|
||||
.bytes = std::move(bytes.value()),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>
|
||||
plan_document_canvas_project_save_target(
|
||||
std::string_view data_directory,
|
||||
std::string_view target_path)
|
||||
{
|
||||
if (data_directory.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save data directory must not be empty"));
|
||||
}
|
||||
if (target_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save target path must not be empty"));
|
||||
}
|
||||
|
||||
const auto basename_start = target_path.find_last_of("/\\");
|
||||
const auto file_name_start = basename_start == std::string_view::npos ? 0U : basename_start + 1U;
|
||||
auto file_name = target_path.substr(file_name_start);
|
||||
if (file_name.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save target file name must not be empty"));
|
||||
}
|
||||
|
||||
constexpr std::string_view ppi_extension = ".ppi";
|
||||
if (file_name.size() > ppi_extension.size()
|
||||
&& file_name.substr(file_name.size() - ppi_extension.size()) == ppi_extension) {
|
||||
file_name.remove_suffix(ppi_extension.size());
|
||||
}
|
||||
|
||||
DocumentCanvasProjectSaveTargetPlan plan;
|
||||
plan.target_path = std::string(target_path);
|
||||
plan.file_name = std::string(file_name);
|
||||
plan.temporary_path.reserve(data_directory.size() + plan.file_name.size() + 10U);
|
||||
plan.temporary_path += data_directory;
|
||||
plan.temporary_path += "/";
|
||||
plan.temporary_path += plan.file_name;
|
||||
plan.temporary_path += ".tmp.ppi";
|
||||
plan.timelapse_path.reserve(data_directory.size() + plan.file_name.size() + 6U);
|
||||
plan.timelapse_path += data_directory;
|
||||
plan.timelapse_path += "/";
|
||||
plan.timelapse_path += plan.file_name;
|
||||
plan.timelapse_path += ".pptl";
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>
|
||||
plan_document_canvas_project_save_write(
|
||||
const DocumentCanvasProjectSaveTargetPlan& target,
|
||||
bool target_exists)
|
||||
{
|
||||
if (target.target_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save write target path must not be empty"));
|
||||
}
|
||||
|
||||
DocumentCanvasProjectSaveWritePlan plan;
|
||||
plan.target_exists = target_exists;
|
||||
plan.target_path = target.target_path;
|
||||
plan.temporary_path = target.temporary_path;
|
||||
|
||||
if (!target_exists) {
|
||||
plan.write_path = target.target_path;
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
if (target.temporary_path.empty()) {
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("project save temporary path must not be empty"));
|
||||
}
|
||||
|
||||
plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap;
|
||||
plan.write_path = target.temporary_path;
|
||||
plan.uses_temporary = true;
|
||||
plan.falls_back_to_direct_on_temporary_open_failure = true;
|
||||
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasProjectSaveCommitPlan plan_document_canvas_project_save_commit(
|
||||
DocumentCanvasProjectSaveCommitInput input) noexcept
|
||||
{
|
||||
DocumentCanvasProjectSaveCommitPlan plan;
|
||||
plan.used_temporary = input.used_temporary;
|
||||
|
||||
if (!input.used_temporary) {
|
||||
plan.saved = true;
|
||||
plan.log_message = "project saved to target";
|
||||
return plan;
|
||||
}
|
||||
|
||||
if (!input.target_remove_attempted || !input.target_remove_succeeded) {
|
||||
plan.log_message = "could not remove target project before temporary swap";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.target_removed = true;
|
||||
if (!input.temporary_rename_attempted || !input.temporary_rename_succeeded) {
|
||||
plan.target_may_be_missing = true;
|
||||
plan.log_message = "temporary project not swapped after original removal";
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.saved = true;
|
||||
plan.temporary_renamed = true;
|
||||
plan.log_message = "temporary project swapped successfully";
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentCanvasProjectSavePostCommitPlan plan_document_canvas_project_save_post_commit(
|
||||
DocumentCanvasProjectSavePostCommitInput input) noexcept
|
||||
{
|
||||
DocumentCanvasProjectSavePostCommitPlan plan;
|
||||
plan.dismisses_progress_ui = input.progress_ui_visible;
|
||||
|
||||
if (!input.save_succeeded) {
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan.marks_document_clean = true;
|
||||
plan.marks_new_document_committed = true;
|
||||
plan.saves_timelapse_sidecar = input.timelapse_encoder_available;
|
||||
plan.flushes_platform_storage = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
|
||||
bool has_canvas,
|
||||
float r = 0.0F,
|
||||
float g = 0.0F,
|
||||
float b = 0.0F,
|
||||
float a = 0.0F) noexcept
|
||||
{
|
||||
const float channels[] { r, g, b, a };
|
||||
for (const float channel : channels) {
|
||||
const auto status = validate_clear_color_channel(channel);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
DocumentCanvasClearPlan plan;
|
||||
plan.r = r;
|
||||
plan.g = g;
|
||||
plan.b = b;
|
||||
plan.a = a;
|
||||
plan.clears_canvas = has_canvas;
|
||||
plan.records_undo = has_canvas;
|
||||
plan.marks_unsaved = has_canvas;
|
||||
plan.no_op = !has_canvas;
|
||||
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
|
||||
const DocumentCanvasClearPlan& plan,
|
||||
DocumentCanvasClearServices& services)
|
||||
{
|
||||
const float channels[] { plan.r, plan.g, plan.b, plan.a };
|
||||
for (const float channel : channels) {
|
||||
const auto status = validate_clear_color_channel(channel);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (plan.no_op || !plan.clears_canvas) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
315
src/app_core/document_cloud.h
Normal file
315
src/app_core/document_cloud.h
Normal file
@@ -0,0 +1,315 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class CloudUploadAction {
|
||||
unavailable_no_canvas,
|
||||
show_save_required_warning,
|
||||
prompt_publish,
|
||||
};
|
||||
|
||||
enum class CloudBrowseAction {
|
||||
unavailable_no_canvas,
|
||||
show_browser,
|
||||
};
|
||||
|
||||
enum class CloudDownloadSelectionAction {
|
||||
wait_for_selection,
|
||||
start_download,
|
||||
};
|
||||
|
||||
enum class CloudTransferDirection {
|
||||
download,
|
||||
upload,
|
||||
};
|
||||
|
||||
enum class CloudTransferAction {
|
||||
reject_missing_source,
|
||||
reject_missing_destination,
|
||||
start_transfer,
|
||||
};
|
||||
|
||||
struct CloudUploadPlan {
|
||||
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
|
||||
bool save_before_upload = false;
|
||||
};
|
||||
|
||||
struct CloudBulkUploadPlan {
|
||||
std::size_t file_count = 0;
|
||||
int progress_total = 0;
|
||||
bool show_progress = false;
|
||||
};
|
||||
|
||||
struct CloudDownloadRequest {
|
||||
std::string selected_file;
|
||||
std::string selected_path;
|
||||
std::string selected_name;
|
||||
};
|
||||
|
||||
struct CloudTransferPlan {
|
||||
CloudTransferDirection direction = CloudTransferDirection::download;
|
||||
CloudTransferAction action = CloudTransferAction::reject_missing_source;
|
||||
bool enable_progress = false;
|
||||
bool disable_tls_verification = false;
|
||||
};
|
||||
|
||||
struct CloudTransferProgressPlan {
|
||||
bool notify = false;
|
||||
float fraction = 0.0F;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_save_required_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Warning",
|
||||
"This document needs to be saved before upload.",
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_publish_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Publish document",
|
||||
"Would you like to upload to the public domain?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_upload_success_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Success",
|
||||
"This document has been succesfully uploaded.",
|
||||
false);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_upload_progress_dialog()
|
||||
{
|
||||
return plan_app_progress_dialog("Uploading", 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_cloud_bulk_upload_progress_dialog(int progress_total)
|
||||
{
|
||||
return plan_app_progress_dialog("Export Pano Image", progress_total);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_cloud_download_progress_prompt()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Downloading",
|
||||
"Download in progress",
|
||||
true);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string format_cloud_download_progress_message(float progress_fraction)
|
||||
{
|
||||
char buffer[64] {};
|
||||
std::snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"Download in progress %.2f%%",
|
||||
progress_fraction * 100.0F);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
class CloudServices {
|
||||
public:
|
||||
virtual ~CloudServices() = default;
|
||||
|
||||
virtual void show_save_required_warning() = 0;
|
||||
virtual void prompt_publish(bool save_before_upload) = 0;
|
||||
virtual void begin_bulk_upload(int progress_total, bool show_progress) = 0;
|
||||
virtual void upload_all_bulk_files() = 0;
|
||||
virtual void end_bulk_upload() = 0;
|
||||
virtual void show_browser() = 0;
|
||||
virtual void start_download(const CloudDownloadRequest& request) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
|
||||
bool has_canvas,
|
||||
bool is_new_document,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
if (!has_canvas) {
|
||||
return { CloudUploadAction::unavailable_no_canvas, false };
|
||||
}
|
||||
|
||||
if (is_new_document) {
|
||||
return { CloudUploadAction::show_save_required_warning, false };
|
||||
}
|
||||
|
||||
return { CloudUploadAction::prompt_publish, has_unsaved_changes };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudBrowseAction plan_cloud_browse(bool has_canvas) noexcept
|
||||
{
|
||||
return has_canvas
|
||||
? CloudBrowseAction::show_browser
|
||||
: CloudBrowseAction::unavailable_no_canvas;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudDownloadSelectionAction plan_cloud_download_selection(
|
||||
std::string_view selected_file) noexcept
|
||||
{
|
||||
return selected_file.empty()
|
||||
? CloudDownloadSelectionAction::wait_for_selection
|
||||
: CloudDownloadSelectionAction::start_download;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudBulkUploadPlan plan_cloud_bulk_upload(
|
||||
std::size_t file_count,
|
||||
bool progress_ui_available) noexcept
|
||||
{
|
||||
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||
return {
|
||||
file_count,
|
||||
file_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(file_count),
|
||||
progress_ui_available,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_download_transfer(
|
||||
std::string_view url,
|
||||
std::string_view destination_path,
|
||||
bool has_progress_callback,
|
||||
bool disables_tls_verification) noexcept
|
||||
{
|
||||
if (url.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::reject_missing_source,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
if (destination_path.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::reject_missing_destination,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
CloudTransferDirection::download,
|
||||
CloudTransferAction::start_transfer,
|
||||
has_progress_callback,
|
||||
disables_tls_verification,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_upload_transfer(
|
||||
std::string_view filename,
|
||||
bool has_progress_callback,
|
||||
bool disables_tls_verification) noexcept
|
||||
{
|
||||
if (filename.empty()) {
|
||||
return {
|
||||
CloudTransferDirection::upload,
|
||||
CloudTransferAction::reject_missing_source,
|
||||
false,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
CloudTransferDirection::upload,
|
||||
CloudTransferAction::start_transfer,
|
||||
has_progress_callback,
|
||||
disables_tls_verification,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloudTransferProgressPlan plan_cloud_transfer_progress(
|
||||
std::int64_t total,
|
||||
std::int64_t current) noexcept
|
||||
{
|
||||
if (total <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto clamped_current = current < 0
|
||||
? std::int64_t { 0 }
|
||||
: (current > total ? total : current);
|
||||
return {
|
||||
true,
|
||||
static_cast<float>(static_cast<double>(clamped_current) / static_cast<double>(total)),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
|
||||
const CloudUploadPlan& plan,
|
||||
CloudServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case CloudUploadAction::unavailable_no_canvas:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudUploadAction::show_save_required_warning:
|
||||
services.show_save_required_warning();
|
||||
return pp::foundation::Status::success();
|
||||
case CloudUploadAction::prompt_publish:
|
||||
services.prompt_publish(plan.save_before_upload);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud upload action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_bulk_upload_plan(
|
||||
const CloudBulkUploadPlan& plan,
|
||||
CloudServices& services)
|
||||
{
|
||||
services.begin_bulk_upload(plan.progress_total, plan.show_progress);
|
||||
services.upload_all_bulk_files();
|
||||
services.end_bulk_upload();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_browse_action(
|
||||
CloudBrowseAction action,
|
||||
CloudServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case CloudBrowseAction::unavailable_no_canvas:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudBrowseAction::show_browser:
|
||||
services.show_browser();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud browse action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_cloud_download_selection_action(
|
||||
CloudDownloadSelectionAction action,
|
||||
CloudServices& services,
|
||||
const CloudDownloadRequest& request)
|
||||
{
|
||||
switch (action) {
|
||||
case CloudDownloadSelectionAction::wait_for_selection:
|
||||
return pp::foundation::Status::success();
|
||||
case CloudDownloadSelectionAction::start_download:
|
||||
if (request.selected_file.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("cloud download requires a selected file");
|
||||
}
|
||||
services.start_download(request);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown cloud download selection action");
|
||||
}
|
||||
|
||||
}
|
||||
1
src/app_core/document_export.cpp
Normal file
1
src/app_core/document_export.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "app_core/document_export.h"
|
||||
1145
src/app_core/document_export.h
Normal file
1145
src/app_core/document_export.h
Normal file
File diff suppressed because it is too large
Load Diff
88
src/app_core/document_import.h
Normal file
88
src/app_core/document_import.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentImageImportAction {
|
||||
import_equirectangular,
|
||||
place_transform,
|
||||
};
|
||||
|
||||
struct DocumentImageImportPlan {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
DocumentImageImportAction action = DocumentImageImportAction::place_transform;
|
||||
bool imports_equirectangular = false;
|
||||
bool enters_transform_mode = false;
|
||||
};
|
||||
|
||||
class DocumentImageImportServices {
|
||||
public:
|
||||
virtual ~DocumentImageImportServices() = default;
|
||||
|
||||
virtual void import_equirectangular(std::string_view path) = 0;
|
||||
virtual void enter_transform_import(std::string_view path) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_document_image_import_dimensions(
|
||||
int width,
|
||||
int height) noexcept
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("image dimensions must be positive");
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentImageImportPlan> plan_document_image_import(
|
||||
int width,
|
||||
int height) noexcept
|
||||
{
|
||||
const auto dimensions = validate_document_image_import_dimensions(width, height);
|
||||
if (!dimensions.ok()) {
|
||||
return pp::foundation::Result<DocumentImageImportPlan>::failure(dimensions);
|
||||
}
|
||||
|
||||
const auto wide_equirect = static_cast<long long>(width) == static_cast<long long>(height) * 2LL;
|
||||
const auto vertical_cube_strip = width == height / 6;
|
||||
|
||||
DocumentImageImportPlan plan;
|
||||
plan.width = width;
|
||||
plan.height = height;
|
||||
plan.imports_equirectangular = wide_equirect || vertical_cube_strip;
|
||||
plan.enters_transform_mode = !plan.imports_equirectangular;
|
||||
plan.action = plan.imports_equirectangular
|
||||
? DocumentImageImportAction::import_equirectangular
|
||||
: DocumentImageImportAction::place_transform;
|
||||
return pp::foundation::Result<DocumentImageImportPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_image_import_plan(
|
||||
const DocumentImageImportPlan& plan,
|
||||
std::string_view path,
|
||||
DocumentImageImportServices& services)
|
||||
{
|
||||
const auto dimensions = validate_document_image_import_dimensions(plan.width, plan.height);
|
||||
if (!dimensions.ok()) {
|
||||
return dimensions;
|
||||
}
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("image import path must not be empty");
|
||||
}
|
||||
|
||||
switch (plan.action) {
|
||||
case DocumentImageImportAction::import_equirectangular:
|
||||
services.import_equirectangular(path);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentImageImportAction::place_transform:
|
||||
services.enter_transform_import(path);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown image import action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
760
src/app_core/document_layer.h
Normal file
760
src/app_core/document_layer.h
Normal file
@@ -0,0 +1,760 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr std::size_t document_layer_name_max_length = 128;
|
||||
inline constexpr int document_layer_legacy_blend_mode_count = 5;
|
||||
|
||||
enum class DocumentLayerRenameAction {
|
||||
no_op_same_name,
|
||||
rename_and_record_undo,
|
||||
};
|
||||
|
||||
enum class DocumentLayerOperation {
|
||||
add,
|
||||
duplicate,
|
||||
select,
|
||||
reorder,
|
||||
remove,
|
||||
set_opacity,
|
||||
set_visibility,
|
||||
set_alpha_lock,
|
||||
set_blend_mode,
|
||||
set_highlight,
|
||||
};
|
||||
|
||||
enum class DocumentLayerMenuCommand {
|
||||
clear,
|
||||
rename,
|
||||
merge_down,
|
||||
};
|
||||
|
||||
enum class DocumentLayerMenuAction {
|
||||
clear_current_layer,
|
||||
show_rename_dialog,
|
||||
merge_with_lower_layer,
|
||||
show_merge_animated_not_supported,
|
||||
no_op_select_layer,
|
||||
no_op_select_upper_layer,
|
||||
};
|
||||
|
||||
struct DocumentLayerRenamePlan {
|
||||
std::string old_name;
|
||||
std::string new_name;
|
||||
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
|
||||
};
|
||||
|
||||
struct DocumentLayerOperationPlan {
|
||||
DocumentLayerOperation operation = DocumentLayerOperation::select;
|
||||
int index = 0;
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
int insert_index = 0;
|
||||
int source_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool flag = false;
|
||||
int blend_mode = 0;
|
||||
bool mutates_document = false;
|
||||
bool marks_unsaved = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_title = false;
|
||||
};
|
||||
|
||||
struct DocumentLayerMenuPlan {
|
||||
DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
|
||||
DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
|
||||
std::string label;
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
};
|
||||
|
||||
struct DocumentLayerMergePlan {
|
||||
int from_index = 0;
|
||||
int to_index = 0;
|
||||
bool create_history = true;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelInput {
|
||||
int layer_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
int blend_mode = 0;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelLayerView {
|
||||
int layer_index = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
int blend_mode = 0;
|
||||
bool current = false;
|
||||
};
|
||||
|
||||
struct DocumentLayerPanelView {
|
||||
int current_index = 0;
|
||||
float current_opacity = 1.0F;
|
||||
bool current_alpha_locked = false;
|
||||
int current_blend_mode = 0;
|
||||
std::vector<DocumentLayerPanelLayerView> layers;
|
||||
};
|
||||
|
||||
class DocumentLayerMenuServices {
|
||||
public:
|
||||
virtual ~DocumentLayerMenuServices() = default;
|
||||
|
||||
virtual void clear_current_layer() = 0;
|
||||
virtual void show_rename_dialog() = 0;
|
||||
virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
|
||||
virtual void show_merge_animated_not_supported() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerRenameServices {
|
||||
public:
|
||||
virtual ~DocumentLayerRenameServices() = default;
|
||||
|
||||
virtual void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) = 0;
|
||||
virtual void set_current_layer_name(std::string_view new_name) = 0;
|
||||
virtual void finish_layer_rename() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerOperationServices {
|
||||
public:
|
||||
virtual ~DocumentLayerOperationServices() = default;
|
||||
|
||||
virtual void add_layer(std::string_view name, int insert_index) = 0;
|
||||
virtual void duplicate_layer(int source_index, int insert_index) = 0;
|
||||
virtual void select_layer(int index) = 0;
|
||||
virtual void reorder_layer(int from_index, int to_index) = 0;
|
||||
virtual void remove_layer(int index) = 0;
|
||||
virtual void set_layer_opacity(int index, float opacity) = 0;
|
||||
virtual void set_layer_visibility(int index, bool visible) = 0;
|
||||
virtual void set_layer_alpha_lock(int index, bool locked) = 0;
|
||||
virtual void set_layer_blend_mode(int index, int blend_mode) = 0;
|
||||
virtual void set_layer_highlight(int index, bool highlighted) = 0;
|
||||
virtual void mark_unsaved() = 0;
|
||||
virtual void reload_animation_layers() = 0;
|
||||
virtual void update_title() = 0;
|
||||
};
|
||||
|
||||
class DocumentLayerMergeServices {
|
||||
public:
|
||||
virtual ~DocumentLayerMergeServices() = default;
|
||||
|
||||
virtual void merge_layers(int from_index, int to_index, bool create_history) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool document_layer_rename_records_history(
|
||||
const DocumentLayerRenamePlan& plan) noexcept
|
||||
{
|
||||
return plan.action == DocumentLayerRenameAction::rename_and_record_undo;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool document_layer_operation_records_history(
|
||||
const DocumentLayerOperationPlan& plan) noexcept
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentLayerOperation::add:
|
||||
case DocumentLayerOperation::duplicate:
|
||||
case DocumentLayerOperation::remove:
|
||||
case DocumentLayerOperation::set_opacity:
|
||||
case DocumentLayerOperation::set_visibility:
|
||||
case DocumentLayerOperation::set_alpha_lock:
|
||||
case DocumentLayerOperation::set_blend_mode:
|
||||
return plan.mutates_document;
|
||||
case DocumentLayerOperation::reorder:
|
||||
return plan.mutates_document;
|
||||
case DocumentLayerOperation::select:
|
||||
case DocumentLayerOperation::set_highlight:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool document_layer_merge_records_history(
|
||||
const DocumentLayerMergePlan& plan) noexcept
|
||||
{
|
||||
return plan.create_history;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
|
||||
}
|
||||
|
||||
if (index < 0 || index >= layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
|
||||
int layer_count,
|
||||
int index) noexcept
|
||||
{
|
||||
if (layer_count < 0) {
|
||||
return pp::foundation::Status::invalid_argument("layer count must not be negative");
|
||||
}
|
||||
|
||||
if (index < 0 || index > layer_count) {
|
||||
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerPanelView> plan_document_layer_panel_view(
|
||||
const std::vector<DocumentLayerPanelInput>& layers,
|
||||
int current_index)
|
||||
{
|
||||
if (layers.empty()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer panel requires at least one layer"));
|
||||
}
|
||||
|
||||
const auto current_status = validate_layer_index(static_cast<int>(layers.size()), current_index);
|
||||
if (!current_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(current_status);
|
||||
}
|
||||
|
||||
DocumentLayerPanelView view;
|
||||
view.current_index = current_index;
|
||||
view.layers.reserve(layers.size());
|
||||
|
||||
for (const auto& input : layers) {
|
||||
const auto index_status = validate_layer_index(static_cast<int>(layers.size()), input.layer_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(index_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(input.opacity) || input.opacity < 0.0F || input.opacity > 1.0F) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||
}
|
||||
|
||||
if (input.blend_mode < 0 || input.blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::failure(
|
||||
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||
}
|
||||
|
||||
const bool current = input.layer_index == current_index;
|
||||
DocumentLayerPanelLayerView layer;
|
||||
layer.layer_index = input.layer_index;
|
||||
layer.name = input.name;
|
||||
layer.opacity = input.opacity;
|
||||
layer.visible = input.visible;
|
||||
layer.alpha_locked = input.alpha_locked;
|
||||
layer.blend_mode = input.blend_mode;
|
||||
layer.current = current;
|
||||
if (current) {
|
||||
view.current_opacity = input.opacity;
|
||||
view.current_alpha_locked = input.alpha_locked;
|
||||
view.current_blend_mode = input.blend_mode;
|
||||
}
|
||||
view.layers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentLayerPanelView>::success(std::move(view));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
|
||||
std::string_view old_name,
|
||||
std::string_view requested_name)
|
||||
{
|
||||
if (requested_name.empty()) {
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer name must not be empty"));
|
||||
}
|
||||
|
||||
if (requested_name.size() > document_layer_name_max_length) {
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
|
||||
}
|
||||
|
||||
DocumentLayerRenamePlan plan;
|
||||
plan.old_name = std::string(old_name);
|
||||
plan.new_name = std::string(requested_name);
|
||||
plan.action = old_name == requested_name
|
||||
? DocumentLayerRenameAction::no_op_same_name
|
||||
: DocumentLayerRenameAction::rename_and_record_undo;
|
||||
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
|
||||
int layer_count,
|
||||
int insert_index,
|
||||
std::string_view name)
|
||||
{
|
||||
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto rename = plan_document_layer_rename({}, name);
|
||||
if (!rename) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::add;
|
||||
plan.insert_index = insert_index;
|
||||
plan.name = std::string(name);
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
|
||||
int layer_count,
|
||||
int source_index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, source_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::duplicate;
|
||||
plan.source_index = source_index;
|
||||
plan.insert_index = source_index + 1;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::select;
|
||||
plan.index = index;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
|
||||
int layer_count,
|
||||
int from_index,
|
||||
int to_index)
|
||||
{
|
||||
auto index_status = validate_layer_index(layer_count, from_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
index_status = validate_layer_index(layer_count, to_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::reorder;
|
||||
plan.from_index = from_index;
|
||||
plan.to_index = to_index;
|
||||
plan.mutates_document = from_index != to_index;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_title = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
|
||||
int layer_count,
|
||||
int index)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (layer_count <= 1) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::remove;
|
||||
plan.index = index;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
|
||||
int layer_count,
|
||||
int index,
|
||||
float opacity)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_opacity;
|
||||
plan.index = index;
|
||||
plan.opacity = opacity;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool visible)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_visibility;
|
||||
plan.index = index;
|
||||
plan.flag = visible;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool locked)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_alpha_lock;
|
||||
plan.index = index;
|
||||
plan.flag = locked;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
|
||||
int layer_count,
|
||||
int index,
|
||||
int blend_mode)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_blend_mode;
|
||||
plan.index = index;
|
||||
plan.blend_mode = blend_mode;
|
||||
plan.mutates_document = true;
|
||||
plan.marks_unsaved = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
|
||||
int layer_count,
|
||||
int index,
|
||||
bool highlight)
|
||||
{
|
||||
const auto index_status = validate_layer_index(layer_count, index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentLayerOperationPlan plan;
|
||||
plan.operation = DocumentLayerOperation::set_highlight;
|
||||
plan.index = index;
|
||||
plan.flag = highlight;
|
||||
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
|
||||
DocumentLayerMenuCommand command,
|
||||
bool has_current_layer,
|
||||
int current_index,
|
||||
int animation_duration,
|
||||
std::string_view current_layer_name,
|
||||
std::string_view lower_layer_name)
|
||||
{
|
||||
if (current_index < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current layer index must not be negative"));
|
||||
}
|
||||
if (animation_duration < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation duration must not be negative"));
|
||||
}
|
||||
|
||||
DocumentLayerMenuPlan plan;
|
||||
plan.command = command;
|
||||
plan.from_index = current_index;
|
||||
plan.to_index = current_index > 0 ? current_index - 1 : 0;
|
||||
|
||||
switch (command) {
|
||||
case DocumentLayerMenuCommand::clear:
|
||||
plan.action = has_current_layer
|
||||
? DocumentLayerMenuAction::clear_current_layer
|
||||
: DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = has_current_layer
|
||||
? "Clear Layer " + std::string(current_layer_name)
|
||||
: "Clear Layer (Select a layer)";
|
||||
break;
|
||||
case DocumentLayerMenuCommand::rename:
|
||||
plan.action = has_current_layer
|
||||
? DocumentLayerMenuAction::show_rename_dialog
|
||||
: DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = has_current_layer
|
||||
? "Rename Layer " + std::string(current_layer_name)
|
||||
: "Rename Layer (Select a layer)";
|
||||
break;
|
||||
case DocumentLayerMenuCommand::merge_down:
|
||||
if (!has_current_layer) {
|
||||
plan.action = DocumentLayerMenuAction::no_op_select_layer;
|
||||
plan.label = "Merge Layer (Select a layer)";
|
||||
} else if (animation_duration > 1) {
|
||||
plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
|
||||
plan.label = "Merge Layer (Animation not supported)";
|
||||
} else if (current_index <= 0) {
|
||||
plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
|
||||
plan.label = "Merge Layer (Select upper layers)";
|
||||
} else {
|
||||
plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
|
||||
plan.label = "Merge with " + std::string(lower_layer_name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMergePlan> plan_document_layer_merge(
|
||||
int layer_count,
|
||||
int from_index,
|
||||
int to_index,
|
||||
int animation_duration,
|
||||
bool create_history = true)
|
||||
{
|
||||
auto index_status = validate_layer_index(layer_count, from_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
|
||||
}
|
||||
|
||||
index_status = validate_layer_index(layer_count, to_index);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (animation_duration < 0) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation duration must not be negative"));
|
||||
}
|
||||
|
||||
if (animation_duration > 1) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animated layer merge is not supported"));
|
||||
}
|
||||
|
||||
if (from_index <= to_index) {
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("layer merge source must be above the destination"));
|
||||
}
|
||||
|
||||
DocumentLayerMergePlan plan;
|
||||
plan.from_index = from_index;
|
||||
plan.to_index = to_index;
|
||||
plan.create_history = create_history;
|
||||
return pp::foundation::Result<DocumentLayerMergePlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_rename_plan(
|
||||
const DocumentLayerRenamePlan& plan,
|
||||
DocumentLayerRenameServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case DocumentLayerRenameAction::no_op_same_name:
|
||||
services.finish_layer_rename();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerRenameAction::rename_and_record_undo:
|
||||
if (plan.new_name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("layer rename plan must include a new name");
|
||||
}
|
||||
if (plan.old_name == plan.new_name) {
|
||||
return pp::foundation::Status::invalid_argument("layer rename plan must change the name");
|
||||
}
|
||||
if (!document_layer_rename_records_history(plan)) {
|
||||
return pp::foundation::Status::invalid_argument(
|
||||
"layer rename plan must record history when the name changes");
|
||||
}
|
||||
services.record_layer_rename_undo(plan.old_name, plan.new_name);
|
||||
services.set_current_layer_name(plan.new_name);
|
||||
services.finish_layer_rename();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document layer rename action");
|
||||
}
|
||||
|
||||
inline void execute_document_layer_operation_side_effects(
|
||||
const DocumentLayerOperationPlan& plan,
|
||||
DocumentLayerOperationServices& services)
|
||||
{
|
||||
if (plan.marks_unsaved)
|
||||
services.mark_unsaved();
|
||||
if (plan.reloads_animation_layers)
|
||||
services.reload_animation_layers();
|
||||
if (plan.updates_title)
|
||||
services.update_title();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_operation_plan(
|
||||
const DocumentLayerOperationPlan& plan,
|
||||
DocumentLayerOperationServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case DocumentLayerOperation::add:
|
||||
if (!plan.mutates_document || plan.name.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("layer add plan must mutate with a name");
|
||||
}
|
||||
services.add_layer(plan.name, plan.insert_index);
|
||||
break;
|
||||
case DocumentLayerOperation::duplicate:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer duplicate plan must mutate the document");
|
||||
}
|
||||
services.duplicate_layer(plan.source_index, plan.insert_index);
|
||||
break;
|
||||
case DocumentLayerOperation::select:
|
||||
services.select_layer(plan.index);
|
||||
break;
|
||||
case DocumentLayerOperation::reorder:
|
||||
if (plan.mutates_document)
|
||||
services.reorder_layer(plan.from_index, plan.to_index);
|
||||
break;
|
||||
case DocumentLayerOperation::remove:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer remove plan must mutate the document");
|
||||
}
|
||||
services.remove_layer(plan.index);
|
||||
break;
|
||||
case DocumentLayerOperation::set_opacity:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer opacity plan must mutate the document");
|
||||
}
|
||||
services.set_layer_opacity(plan.index, plan.opacity);
|
||||
break;
|
||||
case DocumentLayerOperation::set_visibility:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer visibility plan must mutate the document");
|
||||
}
|
||||
services.set_layer_visibility(plan.index, plan.flag);
|
||||
break;
|
||||
case DocumentLayerOperation::set_alpha_lock:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer alpha-lock plan must mutate the document");
|
||||
}
|
||||
services.set_layer_alpha_lock(plan.index, plan.flag);
|
||||
break;
|
||||
case DocumentLayerOperation::set_blend_mode:
|
||||
if (!plan.mutates_document) {
|
||||
return pp::foundation::Status::invalid_argument("layer blend-mode plan must mutate the document");
|
||||
}
|
||||
services.set_layer_blend_mode(plan.index, plan.blend_mode);
|
||||
break;
|
||||
case DocumentLayerOperation::set_highlight:
|
||||
services.set_layer_highlight(plan.index, plan.flag);
|
||||
break;
|
||||
}
|
||||
|
||||
execute_document_layer_operation_side_effects(plan, services);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_merge_plan(
|
||||
const DocumentLayerMergePlan& plan,
|
||||
DocumentLayerMergeServices& services)
|
||||
{
|
||||
if (plan.from_index <= plan.to_index) {
|
||||
return pp::foundation::Status::invalid_argument(
|
||||
"layer merge source must be above the destination");
|
||||
}
|
||||
|
||||
services.merge_layers(plan.from_index, plan.to_index, plan.create_history);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
|
||||
const DocumentLayerMenuPlan& plan,
|
||||
DocumentLayerMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case DocumentLayerMenuAction::clear_current_layer:
|
||||
services.clear_current_layer();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::show_rename_dialog:
|
||||
services.show_rename_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::merge_with_lower_layer:
|
||||
services.merge_with_lower_layer(plan.from_index, plan.to_index);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::show_merge_animated_not_supported:
|
||||
services.show_merge_animated_not_supported();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentLayerMenuAction::no_op_select_layer:
|
||||
case DocumentLayerMenuAction::no_op_select_upper_layer:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document layer menu action");
|
||||
}
|
||||
|
||||
}
|
||||
73
src/app_core/document_platform_io.h
Normal file
73
src/app_core/document_platform_io.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class PickedPathAction {
|
||||
ignore_empty_path,
|
||||
invoke_callback,
|
||||
};
|
||||
|
||||
enum class DisplayFileAction {
|
||||
ignore_empty_path,
|
||||
open_external_file,
|
||||
};
|
||||
|
||||
enum class VirtualKeyboardAction {
|
||||
show_keyboard,
|
||||
hide_keyboard,
|
||||
};
|
||||
|
||||
enum class CursorVisibilityAction {
|
||||
show_cursor,
|
||||
hide_cursor,
|
||||
};
|
||||
|
||||
enum class ClipboardReadAction {
|
||||
read_text,
|
||||
};
|
||||
|
||||
enum class ClipboardWriteAction {
|
||||
write_text,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? PickedPathAction::ignore_empty_path
|
||||
: PickedPathAction::invoke_callback;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? DisplayFileAction::ignore_empty_path
|
||||
: DisplayFileAction::open_external_file;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr VirtualKeyboardAction plan_virtual_keyboard(bool visible) noexcept
|
||||
{
|
||||
return visible
|
||||
? VirtualKeyboardAction::show_keyboard
|
||||
: VirtualKeyboardAction::hide_keyboard;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept
|
||||
{
|
||||
return visible
|
||||
? CursorVisibilityAction::show_cursor
|
||||
: CursorVisibilityAction::hide_cursor;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
|
||||
{
|
||||
return ClipboardReadAction::read_text;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
|
||||
{
|
||||
return ClipboardWriteAction::write_text;
|
||||
}
|
||||
|
||||
}
|
||||
164
src/app_core/document_recording.h
Normal file
164
src/app_core/document_recording.h
Normal file
@@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class RecordingStartAction {
|
||||
start_thread,
|
||||
no_op_already_running,
|
||||
};
|
||||
|
||||
enum class RecordingStopAction {
|
||||
stop_thread,
|
||||
no_op_not_running,
|
||||
};
|
||||
|
||||
struct RecordingClearPlan {
|
||||
bool stop_running_recording = false;
|
||||
bool delete_recorded_files = false;
|
||||
int frame_count_after_clear = 0;
|
||||
};
|
||||
|
||||
struct RecordingExportPlan {
|
||||
std::size_t frame_count = 0;
|
||||
int progress_total = 0;
|
||||
};
|
||||
|
||||
struct RecordingWorkerIterationPlan {
|
||||
bool continue_running = true;
|
||||
bool encode_frame = false;
|
||||
bool clear_dirty_stroke = false;
|
||||
bool update_frame_label = false;
|
||||
};
|
||||
|
||||
class RecordingServices {
|
||||
public:
|
||||
virtual ~RecordingServices() = default;
|
||||
|
||||
virtual void start_thread() = 0;
|
||||
virtual void stop_thread() = 0;
|
||||
virtual void delete_recorded_files() = 0;
|
||||
virtual void set_frame_count(int frame_count) = 0;
|
||||
virtual void update_frame_label() = 0;
|
||||
virtual void begin_export(int progress_total) = 0;
|
||||
virtual void write_mp4(std::string_view path) = 0;
|
||||
virtual void end_export() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
|
||||
{
|
||||
return is_running
|
||||
? RecordingStartAction::no_op_already_running
|
||||
: RecordingStartAction::start_thread;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingStopAction plan_recording_stop(bool is_running) noexcept
|
||||
{
|
||||
return is_running
|
||||
? RecordingStopAction::stop_thread
|
||||
: RecordingStopAction::no_op_not_running;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingClearPlan plan_recording_clear(
|
||||
bool is_running,
|
||||
bool platform_deletes_recorded_files) noexcept
|
||||
{
|
||||
return {
|
||||
is_running,
|
||||
platform_deletes_recorded_files,
|
||||
0,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingExportPlan plan_recording_export(std::size_t frame_count) noexcept
|
||||
{
|
||||
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||
return {
|
||||
frame_count,
|
||||
frame_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(frame_count),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline AppProgressDialogPlan plan_recording_export_progress_dialog(
|
||||
const RecordingExportPlan& plan)
|
||||
{
|
||||
return plan_app_progress_dialog("Exporting MP4 movie", plan.progress_total);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr RecordingWorkerIterationPlan plan_recording_worker_iteration(
|
||||
bool is_running_after_wake,
|
||||
bool has_encoder,
|
||||
bool has_canvas_document) noexcept
|
||||
{
|
||||
const bool encode = is_running_after_wake && has_encoder && has_canvas_document;
|
||||
return {
|
||||
.continue_running = is_running_after_wake,
|
||||
.encode_frame = encode,
|
||||
.clear_dirty_stroke = encode,
|
||||
.update_frame_label = encode,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_start_action(
|
||||
RecordingStartAction action,
|
||||
RecordingServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case RecordingStartAction::start_thread:
|
||||
services.start_thread();
|
||||
return pp::foundation::Status::success();
|
||||
case RecordingStartAction::no_op_already_running:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown recording start action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_stop_action(
|
||||
RecordingStopAction action,
|
||||
RecordingServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case RecordingStopAction::stop_thread:
|
||||
services.stop_thread();
|
||||
return pp::foundation::Status::success();
|
||||
case RecordingStopAction::no_op_not_running:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown recording stop action");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_clear_plan(
|
||||
const RecordingClearPlan& plan,
|
||||
RecordingServices& services)
|
||||
{
|
||||
if (plan.stop_running_recording) {
|
||||
services.stop_thread();
|
||||
}
|
||||
if (plan.delete_recorded_files) {
|
||||
services.delete_recorded_files();
|
||||
}
|
||||
services.set_frame_count(plan.frame_count_after_clear);
|
||||
services.update_frame_label();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_recording_export_plan(
|
||||
const RecordingExportPlan& plan,
|
||||
RecordingServices& services,
|
||||
std::string_view path)
|
||||
{
|
||||
services.begin_export(plan.progress_total);
|
||||
services.write_mp4(path);
|
||||
services.end_export();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
82
src/app_core/document_resize.h
Normal file
82
src/app_core/document_resize.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_status.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
struct DocumentResizeDialogState {
|
||||
int current_resolution = 0;
|
||||
std::string current_resolution_text;
|
||||
int current_resolution_index = 0;
|
||||
};
|
||||
|
||||
struct DocumentResizePlan {
|
||||
int resolution = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool clears_history = false;
|
||||
};
|
||||
|
||||
class DocumentResizeServices {
|
||||
public:
|
||||
virtual ~DocumentResizeServices() = default;
|
||||
|
||||
virtual void resize_document(int width, int height) = 0;
|
||||
virtual void update_title() = 0;
|
||||
virtual void clear_history() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline DocumentResizeDialogState make_document_resize_dialog_state(
|
||||
int current_resolution)
|
||||
{
|
||||
const auto label = document_resolution_label(current_resolution);
|
||||
const auto index = document_resolution_to_index(current_resolution);
|
||||
std::string text = "Current: ";
|
||||
text.append(label ? std::string_view(label.value()) : std::string_view("unknown"));
|
||||
|
||||
return {
|
||||
current_resolution,
|
||||
text,
|
||||
index ? static_cast<int>(index.value()) : static_cast<int>(document_resolution_values.size()),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentResizePlan> plan_document_resize(
|
||||
int selected_resolution_index)
|
||||
{
|
||||
const auto resolution = display_resolution_from_index(selected_resolution_index);
|
||||
if (!resolution) {
|
||||
return pp::foundation::Result<DocumentResizePlan>::failure(resolution.status());
|
||||
}
|
||||
|
||||
const auto value = resolution.value();
|
||||
return pp::foundation::Result<DocumentResizePlan>::success(
|
||||
DocumentResizePlan {
|
||||
value,
|
||||
value,
|
||||
value,
|
||||
true,
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_resize_plan(
|
||||
const DocumentResizePlan& plan,
|
||||
DocumentResizeServices& services)
|
||||
{
|
||||
if (plan.width <= 0 || plan.height <= 0) {
|
||||
return pp::foundation::Status::out_of_range("resize dimensions must be positive");
|
||||
}
|
||||
|
||||
services.resize_document(plan.width, plan.height);
|
||||
services.update_title();
|
||||
if (plan.clears_history) {
|
||||
services.clear_history();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
67
src/app_core/document_route.cpp
Normal file
67
src/app_core/document_route.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "app_core/document_route.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool is_extension_char(char value) noexcept
|
||||
{
|
||||
const auto ch = static_cast<unsigned char>(value);
|
||||
return std::isalnum(ch) != 0 || value == '_';
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
|
||||
{
|
||||
std::string lowered;
|
||||
lowered.reserve(value.size());
|
||||
for (const char ch : value) {
|
||||
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
}
|
||||
return lowered;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
|
||||
{
|
||||
const auto separator = path.find_last_of("/\\");
|
||||
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
|
||||
}
|
||||
|
||||
const auto dot = path.find_last_of('.');
|
||||
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document path must include a file extension"));
|
||||
}
|
||||
|
||||
const std::string_view extension = path.substr(dot + 1U);
|
||||
for (const char ch : extension) {
|
||||
if (!is_extension_char(ch)) {
|
||||
return pp::foundation::Result<DocumentOpenRoute>::failure(
|
||||
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
|
||||
}
|
||||
}
|
||||
|
||||
auto lowered_extension = lowercase_ascii(extension);
|
||||
auto kind = DocumentOpenKind::open_project;
|
||||
if (lowered_extension == "abr") {
|
||||
kind = DocumentOpenKind::import_abr;
|
||||
} else if (lowered_extension == "ppbr") {
|
||||
kind = DocumentOpenKind::import_ppbr;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentOpenRoute>::success(
|
||||
DocumentOpenRoute {
|
||||
.kind = kind,
|
||||
.path = std::string(path),
|
||||
.directory = std::string(path.substr(0U, separator)),
|
||||
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
|
||||
.extension = std::move(lowered_extension),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
27
src/app_core/document_route.h
Normal file
27
src/app_core/document_route.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentOpenKind {
|
||||
import_abr,
|
||||
import_ppbr,
|
||||
open_project,
|
||||
};
|
||||
|
||||
struct DocumentOpenRoute {
|
||||
DocumentOpenKind kind = DocumentOpenKind::open_project;
|
||||
std::string path;
|
||||
std::string directory;
|
||||
std::string name;
|
||||
std::string extension;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
|
||||
std::string_view path);
|
||||
|
||||
}
|
||||
62
src/app_core/document_session.cpp
Normal file
62
src/app_core/document_session.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "app_core/document_session.h"
|
||||
|
||||
namespace pp::app {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] constexpr HistoryUiPlan make_history_clear_effect() noexcept
|
||||
{
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.clears_history = true;
|
||||
plan.updates_memory_label = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr HistoryUiPlan make_history_no_op_effect() noexcept
|
||||
{
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.no_op = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HistoryUiPlan plan_document_open_history(const DocumentOpenRoute& route) noexcept
|
||||
{
|
||||
return route.kind == DocumentOpenKind::open_project
|
||||
? make_history_clear_effect()
|
||||
: make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_close_request_history(CloseRequestDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_save_history(DocumentSaveDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_workflow_history(DocumentWorkflowDecision) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_file_save_history(const DocumentFileSavePlan&) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_document_version_save_history(const DocumentVersionTarget&) noexcept
|
||||
{
|
||||
return make_history_no_op_effect();
|
||||
}
|
||||
|
||||
HistoryUiPlan plan_new_document_history(const NewDocumentPlan&) noexcept
|
||||
{
|
||||
return make_history_clear_effect();
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
576
src/app_core/document_session.h
Normal file
576
src/app_core/document_session.h
Normal file
@@ -0,0 +1,576 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_route.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class ProjectOpenDecision {
|
||||
open_now,
|
||||
prompt_discard_unsaved,
|
||||
};
|
||||
|
||||
enum class CloseRequestDecision {
|
||||
close_now,
|
||||
show_unsaved_prompt,
|
||||
wait_for_existing_prompt,
|
||||
};
|
||||
|
||||
enum class DocumentSaveIntent {
|
||||
save,
|
||||
save_as,
|
||||
save_version,
|
||||
save_dirty_version,
|
||||
};
|
||||
|
||||
enum class DocumentSaveDecision {
|
||||
no_op,
|
||||
show_save_dialog,
|
||||
save_existing,
|
||||
save_version,
|
||||
};
|
||||
|
||||
enum class DocumentWorkflowDecision {
|
||||
unavailable,
|
||||
continue_now,
|
||||
prompt_save_before_continue,
|
||||
};
|
||||
|
||||
enum class DocumentFileWriteDecision {
|
||||
save_now,
|
||||
prompt_overwrite,
|
||||
};
|
||||
|
||||
enum class DocumentOpenPlanAction {
|
||||
open_project_now,
|
||||
prompt_discard_unsaved_project,
|
||||
prompt_import_abr,
|
||||
prompt_import_ppbr,
|
||||
};
|
||||
|
||||
enum class DocumentSessionPromptKind {
|
||||
close_unsaved_document,
|
||||
save_before_workflow_continue,
|
||||
new_document_overwrite,
|
||||
document_file_overwrite,
|
||||
document_save_error,
|
||||
};
|
||||
|
||||
class DocumentOpenServices {
|
||||
public:
|
||||
virtual ~DocumentOpenServices() = default;
|
||||
|
||||
virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0;
|
||||
virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0;
|
||||
virtual void open_project_now(const DocumentOpenRoute& route) = 0;
|
||||
virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0;
|
||||
};
|
||||
|
||||
class CloseRequestServices {
|
||||
public:
|
||||
virtual ~CloseRequestServices() = default;
|
||||
|
||||
virtual void request_close_now() = 0;
|
||||
virtual void show_unsaved_close_prompt() = 0;
|
||||
};
|
||||
|
||||
class DocumentSaveServices {
|
||||
public:
|
||||
virtual ~DocumentSaveServices() = default;
|
||||
|
||||
virtual void show_save_dialog() = 0;
|
||||
virtual void save_existing_document() = 0;
|
||||
virtual void save_document_version() = 0;
|
||||
};
|
||||
|
||||
class DocumentWorkflowServices {
|
||||
public:
|
||||
virtual ~DocumentWorkflowServices() = default;
|
||||
|
||||
virtual void continue_workflow_now() = 0;
|
||||
virtual void prompt_save_before_continue() = 0;
|
||||
};
|
||||
|
||||
struct DocumentFileTarget {
|
||||
std::string name;
|
||||
std::string directory;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentVersionTarget {
|
||||
std::string name;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct DocumentFileSavePlan {
|
||||
DocumentFileTarget target;
|
||||
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||
};
|
||||
|
||||
class DocumentFileSaveServices {
|
||||
public:
|
||||
virtual ~DocumentFileSaveServices() = default;
|
||||
|
||||
virtual void save_document_file(const DocumentFileSavePlan& plan) = 0;
|
||||
virtual void prompt_overwrite_document_file(const DocumentFileSavePlan& plan) = 0;
|
||||
};
|
||||
|
||||
class DocumentVersionSaveServices {
|
||||
public:
|
||||
virtual ~DocumentVersionSaveServices() = default;
|
||||
|
||||
virtual void save_document_version(const DocumentVersionTarget& target) = 0;
|
||||
};
|
||||
|
||||
struct NewDocumentPlan {
|
||||
DocumentFileTarget target;
|
||||
int resolution = 0;
|
||||
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
||||
};
|
||||
|
||||
class NewDocumentServices {
|
||||
public:
|
||||
virtual ~NewDocumentServices() = default;
|
||||
|
||||
virtual void create_new_document(const NewDocumentPlan& plan) = 0;
|
||||
virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] HistoryUiPlan plan_document_open_history(const DocumentOpenRoute& route) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_close_request_history(CloseRequestDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_save_history(DocumentSaveDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_workflow_history(DocumentWorkflowDecision decision) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_file_save_history(const DocumentFileSavePlan& plan) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_document_version_save_history(const DocumentVersionTarget& target) noexcept;
|
||||
[[nodiscard]] HistoryUiPlan plan_new_document_history(const NewDocumentPlan& plan) noexcept;
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt(
|
||||
DocumentSessionPromptKind kind,
|
||||
std::string_view document_name = {})
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentSessionPromptKind::close_unsaved_document:
|
||||
return plan_app_message_dialog(
|
||||
"Unsaved document",
|
||||
"Do you want to close without saving?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
case DocumentSessionPromptKind::save_before_workflow_continue:
|
||||
return plan_app_message_dialog(
|
||||
"Unsaved document",
|
||||
"Would you like to save this document before closing?",
|
||||
true,
|
||||
"Yes",
|
||||
"No");
|
||||
case DocumentSessionPromptKind::new_document_overwrite:
|
||||
return plan_app_message_dialog(
|
||||
"Warning",
|
||||
"A document with this name already exists, continue?",
|
||||
true);
|
||||
case DocumentSessionPromptKind::document_file_overwrite:
|
||||
{
|
||||
std::string message = "Are you sure you want to overwrite ";
|
||||
message += document_name;
|
||||
message += "?";
|
||||
return plan_app_message_dialog("Warning", message, true);
|
||||
}
|
||||
case DocumentSessionPromptKind::document_save_error:
|
||||
return plan_app_message_dialog(
|
||||
"Saving Error",
|
||||
"There was a problem saving the document",
|
||||
false);
|
||||
}
|
||||
|
||||
return plan_app_message_dialog("Warning", "", false);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
|
||||
{
|
||||
return has_unsaved_changes
|
||||
? ProjectOpenDecision::prompt_discard_unsaved
|
||||
: ProjectOpenDecision::open_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
|
||||
DocumentOpenKind kind,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
switch (kind) {
|
||||
case DocumentOpenKind::import_abr:
|
||||
return DocumentOpenPlanAction::prompt_import_abr;
|
||||
case DocumentOpenKind::import_ppbr:
|
||||
return DocumentOpenPlanAction::prompt_import_ppbr;
|
||||
case DocumentOpenKind::open_project:
|
||||
return has_unsaved_changes
|
||||
? DocumentOpenPlanAction::prompt_discard_unsaved_project
|
||||
: DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
return DocumentOpenPlanAction::open_project_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_open_plan(
|
||||
DocumentOpenPlanAction action,
|
||||
const DocumentOpenRoute& route,
|
||||
DocumentOpenServices& services)
|
||||
{
|
||||
switch (action) {
|
||||
case DocumentOpenPlanAction::open_project_now:
|
||||
if (route.kind != DocumentOpenKind::open_project) {
|
||||
return pp::foundation::Status::invalid_argument("open-project action requires a project route");
|
||||
}
|
||||
services.open_project_now(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_discard_unsaved_project:
|
||||
if (route.kind != DocumentOpenKind::open_project) {
|
||||
return pp::foundation::Status::invalid_argument("discard prompt requires a project route");
|
||||
}
|
||||
services.prompt_discard_unsaved_project(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_import_abr:
|
||||
if (route.kind != DocumentOpenKind::import_abr) {
|
||||
return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route");
|
||||
}
|
||||
services.prompt_import_abr(route);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentOpenPlanAction::prompt_import_ppbr:
|
||||
if (route.kind != DocumentOpenKind::import_ppbr) {
|
||||
return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route");
|
||||
}
|
||||
services.prompt_import_ppbr(route);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document open action");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
|
||||
bool has_unsaved_changes,
|
||||
bool close_prompt_already_open) noexcept
|
||||
{
|
||||
if (!has_unsaved_changes) {
|
||||
return CloseRequestDecision::close_now;
|
||||
}
|
||||
|
||||
return close_prompt_already_open
|
||||
? CloseRequestDecision::wait_for_existing_prompt
|
||||
: CloseRequestDecision::show_unsaved_prompt;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_close_request_decision(
|
||||
CloseRequestDecision decision,
|
||||
CloseRequestServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case CloseRequestDecision::close_now:
|
||||
services.request_close_now();
|
||||
return pp::foundation::Status::success();
|
||||
case CloseRequestDecision::show_unsaved_prompt:
|
||||
services.show_unsaved_close_prompt();
|
||||
return pp::foundation::Status::success();
|
||||
case CloseRequestDecision::wait_for_existing_prompt:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown close request decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
|
||||
bool is_new_document,
|
||||
bool has_unsaved_changes,
|
||||
DocumentSaveIntent intent) noexcept
|
||||
{
|
||||
switch (intent) {
|
||||
case DocumentSaveIntent::save:
|
||||
if (is_new_document) {
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
}
|
||||
return has_unsaved_changes
|
||||
? DocumentSaveDecision::save_existing
|
||||
: DocumentSaveDecision::no_op;
|
||||
case DocumentSaveIntent::save_as:
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
case DocumentSaveIntent::save_version:
|
||||
return is_new_document
|
||||
? DocumentSaveDecision::show_save_dialog
|
||||
: DocumentSaveDecision::save_version;
|
||||
case DocumentSaveIntent::save_dirty_version:
|
||||
if (is_new_document) {
|
||||
return DocumentSaveDecision::show_save_dialog;
|
||||
}
|
||||
return has_unsaved_changes
|
||||
? DocumentSaveDecision::save_version
|
||||
: DocumentSaveDecision::no_op;
|
||||
}
|
||||
|
||||
return DocumentSaveDecision::no_op;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_save_decision(
|
||||
DocumentSaveDecision decision,
|
||||
DocumentSaveServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case DocumentSaveDecision::no_op:
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::show_save_dialog:
|
||||
services.show_save_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::save_existing:
|
||||
services.save_existing_document();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentSaveDecision::save_version:
|
||||
services.save_document_version();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document save decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
|
||||
bool has_canvas,
|
||||
bool has_unsaved_changes) noexcept
|
||||
{
|
||||
if (!has_canvas) {
|
||||
return DocumentWorkflowDecision::unavailable;
|
||||
}
|
||||
|
||||
return has_unsaved_changes
|
||||
? DocumentWorkflowDecision::prompt_save_before_continue
|
||||
: DocumentWorkflowDecision::continue_now;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_workflow_decision(
|
||||
DocumentWorkflowDecision decision,
|
||||
DocumentWorkflowServices& services)
|
||||
{
|
||||
switch (decision) {
|
||||
case DocumentWorkflowDecision::unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentWorkflowDecision::continue_now:
|
||||
services.continue_workflow_now();
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentWorkflowDecision::prompt_save_before_continue:
|
||||
services.prompt_save_before_continue();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document workflow decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name)
|
||||
{
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentFileTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
DocumentFileTarget target;
|
||||
target.name = std::string(document_name);
|
||||
target.directory = std::string(work_directory);
|
||||
target.path.reserve(target.directory.size() + target.name.size() + 5);
|
||||
target.path += target.directory;
|
||||
target.path += "/";
|
||||
target.path += target.name;
|
||||
target.path += ".ppi";
|
||||
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
|
||||
bool target_exists) noexcept
|
||||
{
|
||||
return target_exists
|
||||
? DocumentFileWriteDecision::prompt_overwrite
|
||||
: DocumentFileWriteDecision::save_now;
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
auto target = make_document_file_target(work_directory, document_name);
|
||||
if (!target) {
|
||||
return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
|
||||
}
|
||||
|
||||
DocumentFileSavePlan plan;
|
||||
plan.target = std::move(target.value());
|
||||
plan.write_decision = plan_document_file_write(exists(plan.target.path));
|
||||
return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_file_save_plan(
|
||||
const DocumentFileSavePlan& plan,
|
||||
DocumentFileSaveServices& services)
|
||||
{
|
||||
switch (plan.write_decision) {
|
||||
case DocumentFileWriteDecision::save_now:
|
||||
services.save_document_file(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentFileWriteDecision::prompt_overwrite:
|
||||
services.prompt_overwrite_document_file(plan);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown document file save write decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
|
||||
{
|
||||
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
||||
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
|
||||
return pp::foundation::Result<int>::failure(
|
||||
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
|
||||
std::string_view work_directory,
|
||||
std::string_view document_name,
|
||||
int resolution_index,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
const auto resolution = document_resolution_from_index(resolution_index);
|
||||
if (!resolution) {
|
||||
return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
|
||||
}
|
||||
|
||||
auto save_plan = plan_document_file_save(
|
||||
work_directory,
|
||||
document_name,
|
||||
std::forward<ExistsPredicate>(exists));
|
||||
if (!save_plan) {
|
||||
return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
|
||||
}
|
||||
|
||||
NewDocumentPlan plan;
|
||||
plan.target = std::move(save_plan.value().target);
|
||||
plan.resolution = resolution.value();
|
||||
plan.write_decision = save_plan.value().write_decision;
|
||||
return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_new_document_plan(
|
||||
const NewDocumentPlan& plan,
|
||||
NewDocumentServices& services)
|
||||
{
|
||||
switch (plan.write_decision) {
|
||||
case DocumentFileWriteDecision::save_now:
|
||||
services.create_new_document(plan);
|
||||
return pp::foundation::Status::success();
|
||||
case DocumentFileWriteDecision::prompt_overwrite:
|
||||
services.prompt_overwrite_new_document(plan);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown new document write decision");
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
|
||||
{
|
||||
const auto dot = document_name.rfind('.');
|
||||
if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto is_word = [](char ch) noexcept {
|
||||
return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
|
||||
};
|
||||
return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
|
||||
{
|
||||
int value = 0;
|
||||
for (const char ch : suffix) {
|
||||
if (ch < '0' || ch > '9') {
|
||||
break;
|
||||
}
|
||||
|
||||
value = value * 10 + (ch - '0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
|
||||
{
|
||||
char suffix[4] {};
|
||||
std::snprintf(suffix, sizeof(suffix), ".%02d", version);
|
||||
std::string name;
|
||||
name.reserve(base_name.size() + 3);
|
||||
name += base_name;
|
||||
name += suffix;
|
||||
return name;
|
||||
}
|
||||
|
||||
template <typename ExistsPredicate>
|
||||
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
|
||||
std::string_view directory,
|
||||
std::string_view document_name,
|
||||
ExistsPredicate&& exists)
|
||||
{
|
||||
if (directory.empty()) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("directory must not be empty"));
|
||||
}
|
||||
|
||||
if (document_name.empty()) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::invalid_argument("document name must not be empty"));
|
||||
}
|
||||
|
||||
int current = 0;
|
||||
std::string_view base = document_name;
|
||||
if (has_legacy_two_character_version_suffix(document_name)) {
|
||||
const auto dot = document_name.rfind('.');
|
||||
base = document_name.substr(0, dot);
|
||||
current = legacy_version_number(document_name.substr(dot + 1));
|
||||
}
|
||||
|
||||
for (int version = current + 1; version < 99; ++version) {
|
||||
DocumentVersionTarget target;
|
||||
target.name = make_legacy_version_name(base, version);
|
||||
target.path.reserve(directory.size() + target.name.size() + 5);
|
||||
target.path += directory;
|
||||
target.path += "/";
|
||||
target.path += target.name;
|
||||
target.path += ".ppi";
|
||||
if (!exists(target.path)) {
|
||||
return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
||||
pp::foundation::Status::out_of_range("no available document version target"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_document_version_save(
|
||||
const DocumentVersionTarget& target,
|
||||
DocumentVersionSaveServices& services)
|
||||
{
|
||||
if (target.name.empty() || target.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document version target requires a name and path");
|
||||
}
|
||||
|
||||
services.save_document_version(target);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
19
src/app_core/document_sharing.h
Normal file
19
src/app_core/document_sharing.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class DocumentShareAction {
|
||||
show_save_required_warning,
|
||||
share_now,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
|
||||
{
|
||||
return path.empty()
|
||||
? DocumentShareAction::show_save_required_warning
|
||||
: DocumentShareAction::share_now;
|
||||
}
|
||||
|
||||
}
|
||||
209
src/app_core/file_menu.h
Normal file
209
src/app_core/file_menu.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/document_export.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class FileMenuCommand {
|
||||
new_document,
|
||||
import_image,
|
||||
open_project,
|
||||
browse_cloud,
|
||||
save,
|
||||
save_as,
|
||||
save_version,
|
||||
export_jpeg,
|
||||
export_submenu,
|
||||
share,
|
||||
resize,
|
||||
cloud_upload,
|
||||
cloud_browse,
|
||||
};
|
||||
|
||||
enum class FileMenuAction {
|
||||
show_new_document_dialog,
|
||||
pick_image_for_import,
|
||||
pick_project_file,
|
||||
show_cloud_browser_dialog,
|
||||
save_document,
|
||||
show_export_jpeg_dialog,
|
||||
show_export_submenu,
|
||||
share_document,
|
||||
show_resize_dialog,
|
||||
upload_to_cloud,
|
||||
browse_cloud_documents,
|
||||
};
|
||||
|
||||
struct FileMenuPlan {
|
||||
FileMenuCommand command = FileMenuCommand::new_document;
|
||||
FileMenuAction action = FileMenuAction::show_new_document_dialog;
|
||||
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
|
||||
DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg;
|
||||
};
|
||||
|
||||
class FileMenuServices {
|
||||
public:
|
||||
virtual ~FileMenuServices() = default;
|
||||
|
||||
virtual void show_new_document_dialog() = 0;
|
||||
virtual void pick_image_for_import() = 0;
|
||||
virtual void pick_project_file() = 0;
|
||||
virtual void show_cloud_browser_dialog() = 0;
|
||||
virtual void save_document(DocumentSaveIntent intent) = 0;
|
||||
virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0;
|
||||
virtual void show_export_submenu() = 0;
|
||||
virtual void share_document() = 0;
|
||||
virtual void show_resize_dialog() = 0;
|
||||
virtual void upload_to_cloud() = 0;
|
||||
virtual void browse_cloud_documents() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept
|
||||
{
|
||||
FileMenuPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case FileMenuCommand::new_document:
|
||||
plan.action = FileMenuAction::show_new_document_dialog;
|
||||
break;
|
||||
case FileMenuCommand::import_image:
|
||||
plan.action = FileMenuAction::pick_image_for_import;
|
||||
break;
|
||||
case FileMenuCommand::open_project:
|
||||
plan.action = FileMenuAction::pick_project_file;
|
||||
break;
|
||||
case FileMenuCommand::browse_cloud:
|
||||
plan.action = FileMenuAction::show_cloud_browser_dialog;
|
||||
break;
|
||||
case FileMenuCommand::save:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save;
|
||||
break;
|
||||
case FileMenuCommand::save_as:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save_as;
|
||||
break;
|
||||
case FileMenuCommand::save_version:
|
||||
plan.action = FileMenuAction::save_document;
|
||||
plan.save_intent = DocumentSaveIntent::save_version;
|
||||
break;
|
||||
case FileMenuCommand::export_jpeg:
|
||||
plan.action = FileMenuAction::show_export_jpeg_dialog;
|
||||
plan.export_kind = DocumentExportMenuKind::jpeg;
|
||||
break;
|
||||
case FileMenuCommand::export_submenu:
|
||||
plan.action = FileMenuAction::show_export_submenu;
|
||||
break;
|
||||
case FileMenuCommand::share:
|
||||
plan.action = FileMenuAction::share_document;
|
||||
break;
|
||||
case FileMenuCommand::resize:
|
||||
plan.action = FileMenuAction::show_resize_dialog;
|
||||
break;
|
||||
case FileMenuCommand::cloud_upload:
|
||||
plan.action = FileMenuAction::upload_to_cloud;
|
||||
break;
|
||||
case FileMenuCommand::cloud_browse:
|
||||
plan.action = FileMenuAction::browse_cloud_documents;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<FileMenuCommand> parse_file_menu_command(
|
||||
std::string_view command) noexcept
|
||||
{
|
||||
if (command == "new" || command == "new-document") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::new_document);
|
||||
}
|
||||
if (command == "import" || command == "import-image") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::import_image);
|
||||
}
|
||||
if (command == "open" || command == "open-project") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::open_project);
|
||||
}
|
||||
if (command == "browse") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::browse_cloud);
|
||||
}
|
||||
if (command == "save") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save);
|
||||
}
|
||||
if (command == "save-as") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_as);
|
||||
}
|
||||
if (command == "save-version") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_version);
|
||||
}
|
||||
if (command == "export" || command == "export-jpeg") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_jpeg);
|
||||
}
|
||||
if (command == "export-submenu") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_submenu);
|
||||
}
|
||||
if (command == "share") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::share);
|
||||
}
|
||||
if (command == "resize") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::resize);
|
||||
}
|
||||
if (command == "cloud-upload") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_upload);
|
||||
}
|
||||
if (command == "cloud-browse") {
|
||||
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_browse);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<FileMenuCommand>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown file menu command"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan(
|
||||
const FileMenuPlan& plan,
|
||||
FileMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case FileMenuAction::show_new_document_dialog:
|
||||
services.show_new_document_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::pick_image_for_import:
|
||||
services.pick_image_for_import();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::pick_project_file:
|
||||
services.pick_project_file();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_cloud_browser_dialog:
|
||||
services.show_cloud_browser_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::save_document:
|
||||
services.save_document(plan.save_intent);
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_export_jpeg_dialog:
|
||||
services.show_export_jpeg_dialog(plan.export_kind);
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_export_submenu:
|
||||
services.show_export_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::share_document:
|
||||
services.share_document();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::show_resize_dialog:
|
||||
services.show_resize_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::upload_to_cloud:
|
||||
services.upload_to_cloud();
|
||||
return pp::foundation::Status::success();
|
||||
case FileMenuAction::browse_cloud_documents:
|
||||
services.browse_cloud_documents();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown file menu action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
209
src/app_core/grid_ui.h
Normal file
209
src/app_core/grid_ui.h
Normal file
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class GridUiOperation {
|
||||
request_heightmap_pick,
|
||||
load_heightmap,
|
||||
clear_heightmap,
|
||||
reload_heightmap,
|
||||
render_lightmap,
|
||||
commit_heightmap,
|
||||
};
|
||||
|
||||
struct GridUiPlan {
|
||||
GridUiOperation operation = GridUiOperation::request_heightmap_pick;
|
||||
std::string path;
|
||||
int texture_resolution = 0;
|
||||
int sample_count = 0;
|
||||
bool opens_picker = false;
|
||||
bool loads_heightmap = false;
|
||||
bool clears_heightmap = false;
|
||||
bool renders_lightmap = false;
|
||||
bool commits_heightmap = false;
|
||||
bool updates_preview = false;
|
||||
bool updates_ground_opacity = false;
|
||||
bool updates_shading_mode = false;
|
||||
bool shows_unsupported_message = false;
|
||||
bool shows_progress = false;
|
||||
bool mutates_grid_state = false;
|
||||
};
|
||||
|
||||
class GridUiServices {
|
||||
public:
|
||||
virtual ~GridUiServices() = default;
|
||||
|
||||
virtual void request_heightmap_pick() = 0;
|
||||
virtual pp::foundation::Status load_heightmap(std::string_view path, bool raise_ground_opacity) = 0;
|
||||
virtual void clear_heightmap(bool updates_preview) = 0;
|
||||
virtual void render_lightmap(bool shows_unsupported_message, bool renders_lightmap) = 0;
|
||||
virtual void commit_heightmap(bool updates_ground_opacity) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
|
||||
{
|
||||
if (texture_resolution <= 0 || texture_resolution > 16384) {
|
||||
return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept
|
||||
{
|
||||
if (sample_count <= 0 || sample_count > 4096) {
|
||||
return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::request_heightmap_pick;
|
||||
plan.opens_picker = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
|
||||
{
|
||||
if (path.empty()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("heightmap path must not be empty"));
|
||||
}
|
||||
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::load_heightmap;
|
||||
plan.path = std::string(path);
|
||||
plan.loads_heightmap = true;
|
||||
plan.updates_preview = true;
|
||||
plan.updates_ground_opacity = true;
|
||||
plan.mutates_grid_state = true;
|
||||
return pp::foundation::Result<GridUiPlan>::success(std::move(plan));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::clear_heightmap;
|
||||
plan.clears_heightmap = true;
|
||||
plan.updates_preview = has_heightmap;
|
||||
plan.mutates_grid_state = has_heightmap;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
|
||||
{
|
||||
auto plan = plan_grid_heightmap_load(path);
|
||||
if (!plan) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
|
||||
}
|
||||
plan.value().operation = GridUiOperation::reload_heightmap;
|
||||
plan.value().updates_ground_opacity = false;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_lightmap_render(
|
||||
bool has_heightmap,
|
||||
bool supports_float32,
|
||||
bool supports_float16,
|
||||
int texture_resolution,
|
||||
int sample_count)
|
||||
{
|
||||
const auto texture_status = validate_grid_texture_resolution(texture_resolution);
|
||||
if (!texture_status.ok()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(texture_status);
|
||||
}
|
||||
|
||||
const auto sample_status = validate_grid_lightmap_samples(sample_count);
|
||||
if (!sample_status.ok()) {
|
||||
return pp::foundation::Result<GridUiPlan>::failure(sample_status);
|
||||
}
|
||||
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::render_lightmap;
|
||||
plan.texture_resolution = texture_resolution;
|
||||
plan.sample_count = sample_count;
|
||||
if (!supports_float32 && !supports_float16) {
|
||||
plan.shows_unsupported_message = true;
|
||||
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.renders_lightmap = has_heightmap;
|
||||
plan.shows_progress = has_heightmap;
|
||||
plan.updates_shading_mode = has_heightmap;
|
||||
plan.mutates_grid_state = has_heightmap;
|
||||
return pp::foundation::Result<GridUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept
|
||||
{
|
||||
GridUiPlan plan;
|
||||
plan.operation = GridUiOperation::commit_heightmap;
|
||||
plan.commits_heightmap = has_canvas;
|
||||
plan.updates_ground_opacity = has_canvas;
|
||||
plan.mutates_grid_state = has_canvas;
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_grid_ui_plan(
|
||||
const GridUiPlan& plan,
|
||||
GridUiServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case GridUiOperation::request_heightmap_pick:
|
||||
if (!plan.opens_picker) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap pick plan must open a picker");
|
||||
}
|
||||
services.request_heightmap_pick();
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case GridUiOperation::load_heightmap:
|
||||
case GridUiOperation::reload_heightmap:
|
||||
if (!plan.loads_heightmap || plan.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap load plan must provide a path");
|
||||
}
|
||||
return services.load_heightmap(plan.path, plan.updates_ground_opacity);
|
||||
|
||||
case GridUiOperation::clear_heightmap:
|
||||
if (!plan.clears_heightmap) {
|
||||
return pp::foundation::Status::invalid_argument("grid heightmap clear plan must clear heightmap state");
|
||||
}
|
||||
services.clear_heightmap(plan.updates_preview);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case GridUiOperation::render_lightmap:
|
||||
{
|
||||
const auto texture_status = validate_grid_texture_resolution(plan.texture_resolution);
|
||||
if (!texture_status.ok()) {
|
||||
return texture_status;
|
||||
}
|
||||
const auto sample_status = validate_grid_lightmap_samples(plan.sample_count);
|
||||
if (!sample_status.ok()) {
|
||||
return sample_status;
|
||||
}
|
||||
if (!plan.shows_unsupported_message && !plan.renders_lightmap) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
services.render_lightmap(plan.shows_unsupported_message, plan.renders_lightmap);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
case GridUiOperation::commit_heightmap:
|
||||
if (plan.commits_heightmap) {
|
||||
services.commit_heightmap(plan.updates_ground_opacity);
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown grid UI operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
161
src/app_core/history_ui.h
Normal file
161
src/app_core/history_ui.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class HistoryUiOperation {
|
||||
undo,
|
||||
redo,
|
||||
clear,
|
||||
};
|
||||
|
||||
struct HistoryUiPlan {
|
||||
HistoryUiOperation operation = HistoryUiOperation::undo;
|
||||
int undo_count = 0;
|
||||
int redo_count = 0;
|
||||
int memory_bytes = 0;
|
||||
bool invokes_undo = false;
|
||||
bool invokes_redo = false;
|
||||
bool clears_history = false;
|
||||
bool updates_memory_label = false;
|
||||
bool updates_title = false;
|
||||
bool no_op = false;
|
||||
};
|
||||
|
||||
class HistoryUiServices {
|
||||
public:
|
||||
virtual ~HistoryUiServices() = default;
|
||||
|
||||
virtual void invoke_undo() = 0;
|
||||
virtual void invoke_redo() = 0;
|
||||
virtual void clear_history() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
|
||||
{
|
||||
if (value < 0) {
|
||||
return pp::foundation::Status::out_of_range(message);
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
|
||||
{
|
||||
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::undo;
|
||||
plan.undo_count = undo_count;
|
||||
if (undo_count == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.invokes_undo = true;
|
||||
plan.updates_memory_label = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
|
||||
{
|
||||
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::redo;
|
||||
plan.redo_count = redo_count;
|
||||
if (redo_count == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.invokes_redo = true;
|
||||
plan.updates_memory_label = true;
|
||||
plan.updates_title = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
|
||||
int undo_count,
|
||||
int redo_count,
|
||||
int memory_bytes)
|
||||
{
|
||||
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
|
||||
if (!undo_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
|
||||
}
|
||||
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
|
||||
if (!redo_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
|
||||
}
|
||||
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
|
||||
if (!memory_status.ok()) {
|
||||
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
|
||||
}
|
||||
|
||||
HistoryUiPlan plan;
|
||||
plan.operation = HistoryUiOperation::clear;
|
||||
plan.undo_count = undo_count;
|
||||
plan.redo_count = redo_count;
|
||||
plan.memory_bytes = memory_bytes;
|
||||
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
|
||||
plan.no_op = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.clears_history = true;
|
||||
plan.updates_memory_label = true;
|
||||
return pp::foundation::Result<HistoryUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
|
||||
const HistoryUiPlan& plan,
|
||||
HistoryUiServices& services)
|
||||
{
|
||||
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
|
||||
if (!undo_status.ok()) {
|
||||
return undo_status;
|
||||
}
|
||||
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
|
||||
if (!redo_status.ok()) {
|
||||
return redo_status;
|
||||
}
|
||||
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
|
||||
if (!memory_status.ok()) {
|
||||
return memory_status;
|
||||
}
|
||||
|
||||
if (plan.no_op) {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
switch (plan.operation) {
|
||||
case HistoryUiOperation::undo:
|
||||
if (plan.invokes_undo) {
|
||||
services.invoke_undo();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
case HistoryUiOperation::redo:
|
||||
if (plan.invokes_redo) {
|
||||
services.invoke_redo();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
case HistoryUiOperation::clear:
|
||||
if (plan.clears_history) {
|
||||
services.clear_history();
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown history operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
211
src/app_core/main_toolbar.h
Normal file
211
src/app_core/main_toolbar.h
Normal file
@@ -0,0 +1,211 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_dialog.h"
|
||||
#include "app_core/document_canvas.h"
|
||||
#include "app_core/history_ui.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class MainToolbarCommand {
|
||||
open_document,
|
||||
save_document,
|
||||
undo,
|
||||
redo,
|
||||
clear_history,
|
||||
clear_canvas,
|
||||
show_message_box,
|
||||
show_settings,
|
||||
};
|
||||
|
||||
enum class MainToolbarAction {
|
||||
show_open_dialog,
|
||||
show_save_dialog,
|
||||
invoke_undo,
|
||||
invoke_redo,
|
||||
clear_history,
|
||||
clear_canvas,
|
||||
show_message_box,
|
||||
show_settings_dialog,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
struct MainToolbarPlan {
|
||||
MainToolbarCommand command = MainToolbarCommand::open_document;
|
||||
MainToolbarAction action = MainToolbarAction::show_open_dialog;
|
||||
std::string label;
|
||||
bool requires_canvas = false;
|
||||
bool updates_memory_label = false;
|
||||
bool updates_title = false;
|
||||
bool records_undo = false;
|
||||
bool marks_unsaved = false;
|
||||
bool no_op = false;
|
||||
HistoryUiPlan history;
|
||||
DocumentCanvasClearPlan canvas_clear;
|
||||
};
|
||||
|
||||
class MainToolbarServices {
|
||||
public:
|
||||
virtual ~MainToolbarServices() = default;
|
||||
|
||||
virtual void show_open_dialog() = 0;
|
||||
virtual void show_save_dialog() = 0;
|
||||
virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
|
||||
virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
|
||||
virtual void clear_history(const HistoryUiPlan& plan) = 0;
|
||||
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
|
||||
virtual void show_message_box() = 0;
|
||||
virtual void show_settings_dialog() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline AppMessageDialogPlan plan_main_toolbar_message_dialog()
|
||||
{
|
||||
return plan_app_message_dialog(
|
||||
"Just a test message",
|
||||
"Longer description for the error or the message.",
|
||||
true);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<MainToolbarPlan> plan_main_toolbar_command(
|
||||
MainToolbarCommand command,
|
||||
int undo_count = 0,
|
||||
int redo_count = 0,
|
||||
int memory_bytes = 0,
|
||||
bool has_canvas = true)
|
||||
{
|
||||
MainToolbarPlan plan;
|
||||
plan.command = command;
|
||||
|
||||
switch (command) {
|
||||
case MainToolbarCommand::open_document:
|
||||
plan.action = MainToolbarAction::show_open_dialog;
|
||||
plan.label = "Open";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::save_document:
|
||||
plan.action = MainToolbarAction::show_save_dialog;
|
||||
plan.label = "Save";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::undo:
|
||||
{
|
||||
const auto history = plan_history_undo(undo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().invokes_undo
|
||||
? MainToolbarAction::invoke_undo
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.updates_title = history.value().updates_title;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::redo:
|
||||
{
|
||||
const auto history = plan_history_redo(redo_count);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().invokes_redo
|
||||
? MainToolbarAction::invoke_redo
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.updates_title = history.value().updates_title;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::clear_history:
|
||||
{
|
||||
const auto history = plan_history_clear(undo_count, redo_count, memory_bytes);
|
||||
if (!history) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
|
||||
}
|
||||
plan.action = history.value().clears_history
|
||||
? MainToolbarAction::clear_history
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
|
||||
plan.updates_memory_label = history.value().updates_memory_label;
|
||||
plan.no_op = history.value().no_op;
|
||||
plan.history = history.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::clear_canvas:
|
||||
{
|
||||
const auto clear = plan_document_canvas_clear(has_canvas);
|
||||
if (!clear) {
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(clear.status());
|
||||
}
|
||||
plan.action = clear.value().clears_canvas
|
||||
? MainToolbarAction::clear_canvas
|
||||
: MainToolbarAction::no_op_unavailable;
|
||||
plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)";
|
||||
plan.requires_canvas = true;
|
||||
plan.records_undo = clear.value().records_undo;
|
||||
plan.marks_unsaved = clear.value().marks_unsaved;
|
||||
plan.no_op = clear.value().no_op;
|
||||
plan.canvas_clear = clear.value();
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
case MainToolbarCommand::show_message_box:
|
||||
plan.action = MainToolbarAction::show_message_box;
|
||||
plan.label = "Show Message Box";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
|
||||
case MainToolbarCommand::show_settings:
|
||||
plan.action = MainToolbarAction::show_settings_dialog;
|
||||
plan.label = "Settings";
|
||||
return pp::foundation::Result<MainToolbarPlan>::success(plan);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<MainToolbarPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("unknown main toolbar command"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_main_toolbar_plan(
|
||||
const MainToolbarPlan& plan,
|
||||
MainToolbarServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case MainToolbarAction::show_open_dialog:
|
||||
services.show_open_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_save_dialog:
|
||||
services.show_save_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::invoke_undo:
|
||||
services.invoke_undo(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::invoke_redo:
|
||||
services.invoke_redo(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::clear_history:
|
||||
services.clear_history(plan.history);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::clear_canvas:
|
||||
services.clear_canvas(plan.canvas_clear);
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_message_box:
|
||||
services.show_message_box();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::show_settings_dialog:
|
||||
services.show_settings_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case MainToolbarAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown main toolbar action");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
302
src/app_core/quick_ui.h
Normal file
302
src/app_core/quick_ui.h
Normal file
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class QuickUiSlotKind {
|
||||
brush,
|
||||
color,
|
||||
};
|
||||
|
||||
enum class QuickUiOperation {
|
||||
select_slot,
|
||||
open_slot_popup,
|
||||
restore_state,
|
||||
reset_state,
|
||||
};
|
||||
|
||||
struct QuickUiPlan {
|
||||
QuickUiOperation operation = QuickUiOperation::select_slot;
|
||||
QuickUiSlotKind slot_kind = QuickUiSlotKind::brush;
|
||||
int slot_index = 0;
|
||||
int previous_index = 0;
|
||||
int brush_index = 0;
|
||||
int color_index = 0;
|
||||
int slot_count = 0;
|
||||
bool fire_event = false;
|
||||
bool updates_selection = false;
|
||||
bool opens_brush_popup = false;
|
||||
bool opens_color_picker = false;
|
||||
bool invokes_change_callback = false;
|
||||
bool restores_slots = false;
|
||||
bool resets_slots = false;
|
||||
bool redraws_brush_previews = false;
|
||||
bool mutates_quick_state = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewInput {
|
||||
bool ui_rtl = false;
|
||||
float slider_x = 0.0F;
|
||||
float slider_y = 0.0F;
|
||||
float slider_height = 0.0F;
|
||||
float zoom = 1.0F;
|
||||
bool has_pen_mode = false;
|
||||
bool has_line_mode = false;
|
||||
};
|
||||
|
||||
struct QuickSliderPreviewPlan {
|
||||
float cursor_x = 0.0F;
|
||||
float cursor_y = 0.0F;
|
||||
bool updates_pen_mode = false;
|
||||
bool updates_line_mode = false;
|
||||
bool draws_tip = false;
|
||||
bool disables_pen_outline = false;
|
||||
bool redraws_brush_preview = false;
|
||||
bool invokes_change_callback = false;
|
||||
};
|
||||
|
||||
class QuickUiServices {
|
||||
public:
|
||||
virtual ~QuickUiServices() = default;
|
||||
|
||||
virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0;
|
||||
virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0;
|
||||
virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0;
|
||||
virtual void reset_state(bool fire_event) = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slider_float(float value) noexcept
|
||||
{
|
||||
if (!std::isfinite(value)) {
|
||||
return pp::foundation::Status::invalid_argument("quick slider preview value must be finite");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
|
||||
{
|
||||
if (slot_count <= 0) {
|
||||
return pp::foundation::Status::out_of_range("quick slot count must be greater than zero");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept
|
||||
{
|
||||
const auto count_status = validate_quick_slot_count(slot_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (slot_index < 0 || slot_index >= slot_count) {
|
||||
return pp::foundation::Status::out_of_range("quick slot index is out of range");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickSliderPreviewPlan> plan_quick_slider_preview(
|
||||
const QuickSliderPreviewInput& input)
|
||||
{
|
||||
const auto x_status = validate_quick_slider_float(input.slider_x);
|
||||
if (!x_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(x_status);
|
||||
}
|
||||
const auto y_status = validate_quick_slider_float(input.slider_y);
|
||||
if (!y_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(y_status);
|
||||
}
|
||||
const auto height_status = validate_quick_slider_float(input.slider_height);
|
||||
if (!height_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(height_status);
|
||||
}
|
||||
if (input.slider_height < 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview height must not be negative"));
|
||||
}
|
||||
const auto zoom_status = validate_quick_slider_float(input.zoom);
|
||||
if (!zoom_status.ok()) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(zoom_status);
|
||||
}
|
||||
if (input.zoom <= 0.0F) {
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("quick slider preview zoom must be positive"));
|
||||
}
|
||||
|
||||
const float offset = input.ui_rtl ? -100.0F : 100.0F;
|
||||
QuickSliderPreviewPlan plan;
|
||||
plan.cursor_x = (input.slider_x + offset) * input.zoom;
|
||||
plan.cursor_y = (input.slider_y + input.slider_height * 0.5F) * input.zoom;
|
||||
plan.updates_pen_mode = input.has_pen_mode;
|
||||
plan.updates_line_mode = input.has_line_mode;
|
||||
plan.draws_tip = input.has_pen_mode || input.has_line_mode;
|
||||
plan.disables_pen_outline = input.has_pen_mode;
|
||||
plan.redraws_brush_preview = true;
|
||||
plan.invokes_change_callback = true;
|
||||
return pp::foundation::Result<QuickSliderPreviewPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
|
||||
QuickUiSlotKind slot_kind,
|
||||
int current_index,
|
||||
int clicked_index,
|
||||
int slot_count)
|
||||
{
|
||||
const auto current_status = validate_quick_slot_index(current_index, slot_count);
|
||||
if (!current_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(current_status);
|
||||
}
|
||||
|
||||
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
|
||||
if (!clicked_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(clicked_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.slot_kind = slot_kind;
|
||||
plan.slot_index = clicked_index;
|
||||
plan.previous_index = current_index;
|
||||
plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0;
|
||||
plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0;
|
||||
plan.slot_count = slot_count;
|
||||
if (clicked_index != current_index) {
|
||||
plan.operation = QuickUiOperation::select_slot;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
plan.operation = QuickUiOperation::open_slot_popup;
|
||||
plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush;
|
||||
plan.opens_color_picker = slot_kind == QuickUiSlotKind::color;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_restore(
|
||||
int brush_index,
|
||||
int color_index,
|
||||
int slot_count,
|
||||
bool fire_event)
|
||||
{
|
||||
const auto brush_status = validate_quick_slot_index(brush_index, slot_count);
|
||||
if (!brush_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(brush_status);
|
||||
}
|
||||
|
||||
const auto color_status = validate_quick_slot_index(color_index, slot_count);
|
||||
if (!color_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(color_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.operation = QuickUiOperation::restore_state;
|
||||
plan.brush_index = brush_index;
|
||||
plan.color_index = color_index;
|
||||
plan.slot_count = slot_count;
|
||||
plan.fire_event = fire_event;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = fire_event;
|
||||
plan.restores_slots = true;
|
||||
plan.redraws_brush_previews = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_reset(
|
||||
int slot_count,
|
||||
bool fire_event)
|
||||
{
|
||||
const auto count_status = validate_quick_slot_count(slot_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<QuickUiPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
QuickUiPlan plan;
|
||||
plan.operation = QuickUiOperation::reset_state;
|
||||
plan.brush_index = 0;
|
||||
plan.color_index = 0;
|
||||
plan.slot_count = slot_count;
|
||||
plan.fire_event = fire_event;
|
||||
plan.updates_selection = true;
|
||||
plan.invokes_change_callback = fire_event;
|
||||
plan.resets_slots = true;
|
||||
plan.redraws_brush_previews = true;
|
||||
plan.mutates_quick_state = true;
|
||||
return pp::foundation::Result<QuickUiPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan(
|
||||
const QuickUiPlan& plan,
|
||||
QuickUiServices& services)
|
||||
{
|
||||
switch (plan.operation) {
|
||||
case QuickUiOperation::select_slot:
|
||||
if (!plan.updates_selection) {
|
||||
return pp::foundation::Status::invalid_argument("quick select plan must update selection");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::open_slot_popup:
|
||||
if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) {
|
||||
return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup");
|
||||
}
|
||||
if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) {
|
||||
return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.open_slot_popup(plan.slot_kind, plan.slot_index);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::restore_state:
|
||||
if (!plan.restores_slots) {
|
||||
return pp::foundation::Status::invalid_argument("quick restore plan must restore slots");
|
||||
}
|
||||
{
|
||||
const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count);
|
||||
if (!brush_status.ok()) {
|
||||
return brush_status;
|
||||
}
|
||||
const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count);
|
||||
if (!color_status.ok()) {
|
||||
return color_status;
|
||||
}
|
||||
}
|
||||
services.restore_state(plan.brush_index, plan.color_index, plan.fire_event);
|
||||
return pp::foundation::Status::success();
|
||||
|
||||
case QuickUiOperation::reset_state:
|
||||
if (!plan.resets_slots) {
|
||||
return pp::foundation::Status::invalid_argument("quick reset plan must reset slots");
|
||||
}
|
||||
{
|
||||
const auto status = validate_quick_slot_count(plan.slot_count);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
services.reset_state(plan.fire_event);
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown quick UI operation");
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
185
src/app_core/tools_menu.h
Normal file
185
src/app_core/tools_menu.h
Normal file
@@ -0,0 +1,185 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
enum class ToolsMenuCommand {
|
||||
panels,
|
||||
options,
|
||||
clear_grids,
|
||||
reset_camera,
|
||||
shortcuts,
|
||||
sonarpen,
|
||||
};
|
||||
|
||||
enum class ToolsMenuAction {
|
||||
show_panels_submenu,
|
||||
show_options_submenu,
|
||||
clear_grid_overlays,
|
||||
reset_camera,
|
||||
show_shortcuts_dialog,
|
||||
start_sonarpen,
|
||||
no_op_unavailable,
|
||||
};
|
||||
|
||||
enum class ToolsPanel {
|
||||
presets,
|
||||
color,
|
||||
color_advanced,
|
||||
layers,
|
||||
brush,
|
||||
grids,
|
||||
animation,
|
||||
};
|
||||
|
||||
enum class ToolsPanelAction {
|
||||
open_floating_panel,
|
||||
no_op_already_visible,
|
||||
};
|
||||
|
||||
struct ToolsMenuPlan {
|
||||
ToolsMenuCommand command = ToolsMenuCommand::panels;
|
||||
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
|
||||
std::string_view label;
|
||||
bool closes_root_popup = false;
|
||||
};
|
||||
|
||||
struct ToolsPanelPlan {
|
||||
ToolsPanel panel = ToolsPanel::presets;
|
||||
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
|
||||
std::string_view title;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int min_width = 0;
|
||||
int min_height = 0;
|
||||
bool droppable = true;
|
||||
bool hides_embedded_title = false;
|
||||
};
|
||||
|
||||
class ToolsMenuServices {
|
||||
public:
|
||||
virtual ~ToolsMenuServices() = default;
|
||||
|
||||
virtual void show_panels_submenu() = 0;
|
||||
virtual void show_options_submenu() = 0;
|
||||
virtual void clear_grid_overlays() = 0;
|
||||
virtual void reset_camera() = 0;
|
||||
virtual void show_shortcuts_dialog() = 0;
|
||||
virtual void start_sonarpen() = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
|
||||
ToolsMenuCommand command,
|
||||
bool sonarpen_available = false) noexcept
|
||||
{
|
||||
switch (command) {
|
||||
case ToolsMenuCommand::panels:
|
||||
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
|
||||
case ToolsMenuCommand::options:
|
||||
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
|
||||
case ToolsMenuCommand::clear_grids:
|
||||
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
|
||||
case ToolsMenuCommand::reset_camera:
|
||||
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
|
||||
case ToolsMenuCommand::shortcuts:
|
||||
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
|
||||
case ToolsMenuCommand::sonarpen:
|
||||
return {
|
||||
command,
|
||||
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
|
||||
"SonarPen",
|
||||
sonarpen_available,
|
||||
};
|
||||
}
|
||||
|
||||
return { command, ToolsMenuAction::no_op_unavailable, "", false };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
|
||||
ToolsPanel panel,
|
||||
bool already_visible) noexcept
|
||||
{
|
||||
ToolsPanelPlan plan;
|
||||
plan.panel = panel;
|
||||
plan.action = already_visible
|
||||
? ToolsPanelAction::no_op_already_visible
|
||||
: ToolsPanelAction::open_floating_panel;
|
||||
|
||||
switch (panel) {
|
||||
case ToolsPanel::presets:
|
||||
plan.title = "Brushes";
|
||||
plan.height = 300;
|
||||
plan.min_height = 300;
|
||||
plan.min_width = 100;
|
||||
break;
|
||||
case ToolsPanel::color:
|
||||
plan.title = "Color Picker";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::color_advanced:
|
||||
plan.title = "Color Picker";
|
||||
plan.width = 300;
|
||||
plan.height = 300;
|
||||
break;
|
||||
case ToolsPanel::layers:
|
||||
plan.title = "Layers";
|
||||
plan.height = 300;
|
||||
plan.min_height = 100;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::brush:
|
||||
plan.title = "Brush Settings";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::grids:
|
||||
plan.title = "Grid";
|
||||
plan.height = 300;
|
||||
plan.hides_embedded_title = true;
|
||||
break;
|
||||
case ToolsPanel::animation:
|
||||
plan.title = "Animation";
|
||||
plan.width = 500;
|
||||
plan.height = 300;
|
||||
plan.droppable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
|
||||
const ToolsMenuPlan& plan,
|
||||
ToolsMenuServices& services)
|
||||
{
|
||||
switch (plan.action) {
|
||||
case ToolsMenuAction::show_panels_submenu:
|
||||
services.show_panels_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::show_options_submenu:
|
||||
services.show_options_submenu();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::clear_grid_overlays:
|
||||
services.clear_grid_overlays();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::reset_camera:
|
||||
services.reset_camera();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::show_shortcuts_dialog:
|
||||
services.show_shortcuts_dialog();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::start_sonarpen:
|
||||
services.start_sonarpen();
|
||||
return pp::foundation::Status::success();
|
||||
case ToolsMenuAction::no_op_unavailable:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("unknown tools menu action");
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,520 +1,694 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "app_core/app_frame.h"
|
||||
#include "app_core/app_input.h"
|
||||
#include "app_core/document_platform_io.h"
|
||||
#include "app_core/document_sharing.h"
|
||||
#include "platform_api/platform_services.h"
|
||||
#include "platform_legacy/legacy_platform_services.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
void displayKeyboard(bool pShow);
|
||||
void android_pick_file(std::function<void(std::string)> callback);
|
||||
void android_pick_file_save(std::function<void(std::string)> callback);
|
||||
std::string android_get_clipboard();
|
||||
bool android_set_clipboard(const std::string& s);
|
||||
#elif _WIN32
|
||||
std::string win32_open_file(const char* filter);
|
||||
std::string win32_save_file(const char* filter);
|
||||
std::string win32_open_dir();
|
||||
void win32_show_cursor(bool visible);
|
||||
bool win32_clipboard_set_text(const std::string & s);
|
||||
std::string win32_clipboard_get_text();
|
||||
#elif __APPLE__
|
||||
#elif __LINUX__
|
||||
#include <tinyfiledialogs.h>
|
||||
#elif __WEB__
|
||||
void webgl_pick_file(std::function<void(std::string)> callback);
|
||||
void webgl_pick_file_save(const std::string& path,
|
||||
const std::string& name, std::function<void(bool)> callback);
|
||||
void webgl_sync();
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] GLint rgba8_internal_format() noexcept
|
||||
{
|
||||
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
|
||||
{
|
||||
const auto action = pp::app::plan_virtual_keyboard(visible);
|
||||
return visible
|
||||
? action == pp::app::VirtualKeyboardAction::show_keyboard
|
||||
: action == pp::app::VirtualKeyboardAction::hide_keyboard;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept
|
||||
{
|
||||
const auto action = pp::app::plan_cursor_visibility(visible);
|
||||
return visible
|
||||
? action == pp::app::CursorVisibilityAction::show_cursor
|
||||
: action == pp::app::CursorVisibilityAction::hide_cursor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
|
||||
{
|
||||
if (App::I)
|
||||
{
|
||||
if (auto* services = App::I->platform_services())
|
||||
return *services;
|
||||
}
|
||||
return pp::platform::legacy::platform_services();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
|
||||
{
|
||||
platform_services_ = services;
|
||||
}
|
||||
|
||||
pp::platform::PlatformServices* App::platform_services() const noexcept
|
||||
{
|
||||
return platform_services_;
|
||||
}
|
||||
|
||||
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
|
||||
{
|
||||
return active_platform_services().prepare_storage_paths();
|
||||
}
|
||||
|
||||
std::string App::clipboard_get_text()
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_get_text();
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_get_string];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_get_string];
|
||||
#elif __ANDROID__
|
||||
return android_get_clipboard();
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
|
||||
return {};
|
||||
|
||||
return active_platform_services().clipboard_text();
|
||||
}
|
||||
|
||||
bool App::clipboard_set_text(const std::string& s)
|
||||
{
|
||||
#if _WIN32
|
||||
return win32_clipboard_set_text(s);
|
||||
#elif __IOS__
|
||||
return [ios_view clipboard_set_string:s];
|
||||
#elif __OSX__
|
||||
return [osx_view clipboard_set_string:s];
|
||||
#elif __ANDROID__
|
||||
return android_set_clipboard(s);
|
||||
#endif
|
||||
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
|
||||
return false;
|
||||
|
||||
return active_platform_services().set_clipboard_text(s);
|
||||
}
|
||||
|
||||
void App::stacktrace()
|
||||
{
|
||||
#if __OSX__
|
||||
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
|
||||
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
#endif
|
||||
active_platform_services().log_stacktrace();
|
||||
}
|
||||
|
||||
void App::crash_test()
|
||||
{
|
||||
#ifdef __IOS__
|
||||
[ios_view crash];
|
||||
#elif __OSX__
|
||||
[osx_view hockeyapp_crash];
|
||||
#elif defined(_WIN32)
|
||||
__debugbreak();
|
||||
#elif defined(__ANDROID__)
|
||||
int *x = nullptr; *x = 42;
|
||||
LOG("%d", *x);
|
||||
#endif
|
||||
active_platform_services().trigger_crash_test();
|
||||
}
|
||||
|
||||
void App::tick(float dt)
|
||||
{
|
||||
if (auto* main = layout_designer[main_id])
|
||||
const auto tick_plan = pp::app::plan_app_frame_tick(
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (auto* main = layout_designer[main_id]; tick_plan.tick_designer_layout && main)
|
||||
main->tick(dt);
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; tick_plan.tick_main_layout && main)
|
||||
main->tick(dt);
|
||||
}
|
||||
|
||||
void App::resize(float w, float h)
|
||||
{
|
||||
LOG("App::resize %d %d", (int)w, (int)h);
|
||||
uirtt.create(w, h, -1, GL_RGBA8, true);
|
||||
redraw = true;
|
||||
width = w;
|
||||
height = h;
|
||||
const auto resize_plan = pp::app::plan_app_resize(w, h);
|
||||
if (!resize_plan) {
|
||||
LOG("App resize plan failed: %s", resize_plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resize_plan.value().recreate_ui_render_target) {
|
||||
uirtt.create(
|
||||
resize_plan.value().render_target_width,
|
||||
resize_plan.value().render_target_height,
|
||||
-1,
|
||||
rgba8_internal_format(),
|
||||
true);
|
||||
}
|
||||
redraw = resize_plan.value().request_redraw;
|
||||
width = resize_plan.value().width;
|
||||
height = resize_plan.value().height;
|
||||
}
|
||||
|
||||
void App::show_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(true);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:true];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(true);
|
||||
}
|
||||
|
||||
void App::hide_cursor()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_show_cursor(false);
|
||||
#elif __OSX__
|
||||
[osx_view show_cursor:false];
|
||||
#endif
|
||||
if (!should_dispatch_cursor_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_cursor_visible(false);
|
||||
}
|
||||
|
||||
void App::showKeyboard()
|
||||
{
|
||||
LOG("show keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view show_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(true);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(true))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(true);
|
||||
}
|
||||
|
||||
void App::hideKeyboard()
|
||||
{
|
||||
LOG("hide keyboard");
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view hide_keyboard];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
displayKeyboard(false);
|
||||
#endif
|
||||
if (!should_dispatch_keyboard_visibility(false))
|
||||
return;
|
||||
|
||||
active_platform_services().set_virtual_keyboard_visible(false);
|
||||
}
|
||||
|
||||
void App::pick_image(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view pick_photo:callback];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
|
||||
std::string path = [osx_view pick_file:fileTypes];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif _WIN32
|
||||
std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
callback(p);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#endif
|
||||
active_platform_services().pick_image(std::move(callback));
|
||||
}
|
||||
|
||||
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
||||
for (const auto& t : types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
[ios_view pick_file:fileTypes then:callback];
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
|
||||
for (const auto& t : types)
|
||||
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
|
||||
std::string path = [osx_view pick_file:fileTypes];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
android_pick_file(callback);
|
||||
#elif _WIN32
|
||||
std::string filter = "Supported Files (";
|
||||
bool first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.append(")");
|
||||
filter.push_back(0);
|
||||
first_type = true;
|
||||
for (auto& t : types)
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
||||
first_type = false;
|
||||
}
|
||||
filter.push_back(0);
|
||||
std::string path = win32_open_file(filter.c_str());
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#elif __LINUX__
|
||||
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
|
||||
callback(p);
|
||||
#elif __WEB__
|
||||
webgl_pick_file(callback);
|
||||
#endif
|
||||
active_platform_services().pick_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
#if __IOS__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
std::string ext = "." + type;
|
||||
std::string path = tmp_path + "/" + default_name + ext;
|
||||
std::thread([=]{
|
||||
writer(path);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view pick_file_save:path];
|
||||
});
|
||||
callback(path, true);
|
||||
}).detach();
|
||||
}
|
||||
#elif __WEB__
|
||||
void App::pick_file_save(const std::string& type, const std::string& default_name,
|
||||
std::function<void(std::string)> writer, std::function<void(const std::string& path, bool saved)> callback)
|
||||
const auto target = active_platform_services().prepare_writable_file(type, default_name, data_path, tmp_path);
|
||||
if (target.path.empty())
|
||||
{
|
||||
redraw = true;
|
||||
auto path = data_path + "/" + default_name + "." + type;
|
||||
LOG("App::pick_file_save %s", path.c_str());
|
||||
writer(path);
|
||||
webgl_pick_file_save(path, default_name + "." + type, callback);
|
||||
callback({}, false);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
|
||||
LOG("App::pick_file_save %s", target.path.c_str());
|
||||
auto write_and_save = [=] {
|
||||
writer(target.path);
|
||||
save_prepared_file(target.path, target.suggested_name, callback);
|
||||
};
|
||||
|
||||
if (target.write_on_background_thread)
|
||||
std::thread(write_and_save).detach();
|
||||
else
|
||||
write_and_save();
|
||||
}
|
||||
|
||||
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)
|
||||
active_platform_services().pick_save_file(std::move(types), std::move(callback));
|
||||
}
|
||||
|
||||
bool App::uses_prepared_file_writes() const
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
|
||||
first_type = false;
|
||||
return active_platform_services().uses_prepared_file_writes();
|
||||
}
|
||||
filter.append(")");
|
||||
filter.push_back(0);
|
||||
first_type = true;
|
||||
for (auto& t : types)
|
||||
|
||||
bool App::uses_work_directory_document_export_collections() const
|
||||
{
|
||||
filter.append(std::string(first_type ? "" : ";") + "*." + t);
|
||||
first_type = false;
|
||||
return active_platform_services().uses_work_directory_document_export_collections();
|
||||
}
|
||||
filter.push_back(0);
|
||||
std::string path = win32_save_file(filter.c_str());
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#endif
|
||||
|
||||
bool App::disables_network_tls_verification() const
|
||||
{
|
||||
return active_platform_services().disables_network_tls_verification();
|
||||
}
|
||||
|
||||
bool App::uses_ppbr_export_data_directory_override() const
|
||||
{
|
||||
return active_platform_services().uses_ppbr_export_data_directory_override();
|
||||
}
|
||||
|
||||
bool App::platform_supports_sonarpen() const
|
||||
{
|
||||
return active_platform_services().supports_sonarpen();
|
||||
}
|
||||
|
||||
void App::start_platform_sonarpen()
|
||||
{
|
||||
active_platform_services().start_sonarpen();
|
||||
}
|
||||
|
||||
int App::default_canvas_resolution() const
|
||||
{
|
||||
return active_platform_services().default_canvas_resolution();
|
||||
}
|
||||
|
||||
bool App::draws_canvas_tip_for_input(kEventSource source, kEventType type) const
|
||||
{
|
||||
return active_platform_services().draws_canvas_tip_for_pointer(
|
||||
source == kEventSource::Mouse,
|
||||
source == kEventSource::Stylus,
|
||||
type == kEventType::MouseUpL);
|
||||
}
|
||||
|
||||
float App::adjust_canvas_input_pressure(float pressure) const
|
||||
{
|
||||
return active_platform_services().adjust_canvas_input_pressure(pressure);
|
||||
}
|
||||
#endif
|
||||
|
||||
void App::pick_dir(std::function<void(std::string path)> callback)
|
||||
{
|
||||
redraw = true;
|
||||
#ifdef __IOS__
|
||||
// NOT IMPLEMENTED
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
std::string path = [osx_view pick_dir];
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
});
|
||||
#elif __ANDROID__
|
||||
// NOT IMPLEMENTED
|
||||
#elif _WIN32
|
||||
// TODO: to be implemented
|
||||
std::string path = win32_open_dir();
|
||||
if (!path.empty())
|
||||
callback(path);
|
||||
#endif
|
||||
active_platform_services().pick_directory(std::move(callback));
|
||||
}
|
||||
|
||||
bool App::supports_working_directory_picker() const
|
||||
{
|
||||
return active_platform_services().supports_working_directory_picker();
|
||||
}
|
||||
|
||||
std::string App::format_working_directory_path(std::string_view path) const
|
||||
{
|
||||
return active_platform_services().format_working_directory_path(path);
|
||||
}
|
||||
|
||||
void App::display_file(std::string path)
|
||||
{
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view display_file:path];
|
||||
});
|
||||
#elif __OSX__
|
||||
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:path.c_str()]];
|
||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// std::string path = [osx_view pick_file];
|
||||
// if (!path.empty())
|
||||
// callback(path);
|
||||
// });
|
||||
#elif __ANDROID__
|
||||
//displayKeyboard(and_app, false);
|
||||
#elif _WIN32
|
||||
// std::string path = win32_open_file();
|
||||
// if (!path.empty())
|
||||
// callback(path);
|
||||
#endif
|
||||
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
|
||||
return;
|
||||
|
||||
active_platform_services().display_file(path);
|
||||
}
|
||||
|
||||
void App::share_file(std::string path)
|
||||
{
|
||||
if (path.empty())
|
||||
const auto plan = pp::app::plan_document_share(path);
|
||||
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
|
||||
{
|
||||
message_box("Sharing failed", "Please save the document before sharing it.");
|
||||
return;
|
||||
}
|
||||
#ifdef __IOS__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
active_platform_services().share_file(path);
|
||||
}
|
||||
|
||||
void App::request_app_close()
|
||||
{
|
||||
active_platform_services().request_app_close();
|
||||
}
|
||||
|
||||
bool App::start_platform_vr_mode()
|
||||
{
|
||||
return active_platform_services().start_vr_mode();
|
||||
}
|
||||
|
||||
void App::stop_platform_vr_mode()
|
||||
{
|
||||
active_platform_services().stop_vr_mode();
|
||||
}
|
||||
|
||||
void App::attach_ui_thread()
|
||||
{
|
||||
active_platform_services().attach_ui_thread();
|
||||
}
|
||||
|
||||
void App::detach_ui_thread()
|
||||
{
|
||||
active_platform_services().detach_ui_thread();
|
||||
}
|
||||
|
||||
void App::acquire_render_context()
|
||||
{
|
||||
active_platform_services().acquire_render_context();
|
||||
}
|
||||
|
||||
void App::release_render_context()
|
||||
{
|
||||
active_platform_services().release_render_context();
|
||||
}
|
||||
|
||||
void App::present_render_context()
|
||||
{
|
||||
active_platform_services().present_render_context();
|
||||
}
|
||||
|
||||
void App::bind_default_render_target()
|
||||
{
|
||||
active_platform_services().bind_default_render_target();
|
||||
}
|
||||
|
||||
void App::bind_main_render_target()
|
||||
{
|
||||
active_platform_services().bind_main_render_target();
|
||||
}
|
||||
|
||||
void App::apply_render_platform_hints()
|
||||
{
|
||||
active_platform_services().apply_render_platform_hints();
|
||||
}
|
||||
|
||||
void App::install_render_debug_callback()
|
||||
{
|
||||
active_platform_services().install_render_debug_callback();
|
||||
}
|
||||
|
||||
void App::begin_render_capture_frame()
|
||||
{
|
||||
active_platform_services().begin_render_capture_frame();
|
||||
}
|
||||
|
||||
void App::end_render_capture_frame()
|
||||
{
|
||||
active_platform_services().end_render_capture_frame();
|
||||
}
|
||||
|
||||
bool App::platform_deletes_recorded_files_on_clear()
|
||||
{
|
||||
return active_platform_services().deletes_recorded_files_on_clear();
|
||||
}
|
||||
|
||||
void App::clear_platform_recorded_files(std::string path)
|
||||
{
|
||||
active_platform_services().clear_recorded_files(path);
|
||||
}
|
||||
|
||||
void App::publish_exported_image(std::string path)
|
||||
{
|
||||
active_platform_services().publish_exported_image(path);
|
||||
}
|
||||
|
||||
void App::flush_platform_storage()
|
||||
{
|
||||
active_platform_services().flush_persistent_storage();
|
||||
}
|
||||
|
||||
std::vector<std::string> App::document_browse_roots() const
|
||||
{
|
||||
return active_platform_services().document_browse_roots(work_path, data_path);
|
||||
}
|
||||
|
||||
void App::save_platform_ui_state()
|
||||
{
|
||||
active_platform_services().save_ui_state();
|
||||
}
|
||||
|
||||
bool App::platform_enables_live_asset_reloading()
|
||||
{
|
||||
return active_platform_services().enables_live_asset_reloading();
|
||||
}
|
||||
|
||||
void App::update_platform_frame(float delta_time_seconds)
|
||||
{
|
||||
active_platform_services().update_platform_frame(delta_time_seconds);
|
||||
}
|
||||
|
||||
void App::report_rendered_frames(int frames)
|
||||
{
|
||||
active_platform_services().report_rendered_frames(frames);
|
||||
}
|
||||
|
||||
void App::save_prepared_file(
|
||||
std::string path,
|
||||
std::string suggested_name,
|
||||
std::function<void(const std::string& path, bool saved)> callback)
|
||||
{
|
||||
active_platform_services().save_prepared_file(
|
||||
path,
|
||||
suggested_name,
|
||||
[callback = std::move(callback)](std::string saved_path, bool saved) {
|
||||
callback(saved_path, saved);
|
||||
});
|
||||
#elif __OSX__
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
|
||||
});
|
||||
#elif __ANDROID__
|
||||
#elif _WIN32
|
||||
// not implemented
|
||||
#endif
|
||||
}
|
||||
|
||||
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse down dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseDownR : kEventType::MouseDownL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_move(float x, float y, float pressure, kEventSource source, bool eraser)
|
||||
{
|
||||
cursor = { x / zoom, y / zoom };
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
cursor = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseMove;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_pressure = pressure;
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_up(int button, float x, float y, kEventSource source, bool eraser)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse up dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = button ? kEventType::MouseUpR : kEventType::MouseUpL;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_source = source;
|
||||
e.m_eraser = eraser;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_scroll(float x, float y, float delta)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_pointer_dispatch(
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Mouse scroll dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseScroll;
|
||||
e.m_pos = { x / zoom, y / zoom };
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_scroll_delta = delta;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.value().dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::mouse_cancel(int button)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_mouse_cancel_dispatch(
|
||||
layout_designer.get(main_id) != nullptr,
|
||||
layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
MouseEvent e;
|
||||
e.m_type = kEventType::MouseCancel;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout_designer[main_id])
|
||||
if (auto* main = layout_designer[main_id]; plan.dispatch_designer_first && main)
|
||||
return main->on_event(&e) == kEventResult::Consumed;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main_if_not_consumed && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_start(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture start dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureStart;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
gesture_p0 = p0;
|
||||
gesture_p1 = p1;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_move(const glm::vec2& p0, const glm::vec2& p1)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_gesture_dispatch(
|
||||
p0.x,
|
||||
p0.y,
|
||||
p1.x,
|
||||
p1.y,
|
||||
gesture_p0.x,
|
||||
gesture_p0.y,
|
||||
gesture_p1.x,
|
||||
gesture_p1.y,
|
||||
zoom,
|
||||
layout.get(main_id) != nullptr);
|
||||
if (!plan) {
|
||||
LOG("Gesture move dispatch plan failed: %s", plan.status().message);
|
||||
return false;
|
||||
}
|
||||
|
||||
redraw = plan.value().request_redraw;
|
||||
GestureEvent e;
|
||||
glm::vec2 p = glm::lerp(p0, p1, 0.5f);
|
||||
e.m_type = kEventType::GestureMove;
|
||||
e.m_pos = p / glm::vec2(zoom);
|
||||
e.m_distance = glm::distance(p0, p1);
|
||||
e.m_distance_delta = e.m_distance - glm::distance(gesture_p0, gesture_p1);
|
||||
e.m_pos_delta = p - glm::lerp(gesture_p0, gesture_p1, 0.5f);
|
||||
e.m_pos = { plan.value().normalized_x, plan.value().normalized_y };
|
||||
e.m_distance = plan.value().distance;
|
||||
e.m_distance_delta = plan.value().distance_delta;
|
||||
e.m_pos_delta = { plan.value().position_delta_x, plan.value().position_delta_y };
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.value().dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::gesture_end()
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
GestureEvent e;
|
||||
e.m_type = kEventType::GestureEnd;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::touch_tap(const glm::vec2& pos, int fingers, int tap_count)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
TouchEvent e;
|
||||
e.m_type = kEventType::TouchTap;
|
||||
e.m_finger_count = fingers;
|
||||
e.m_tap_count = tap_count;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_down(kKey key)
|
||||
{
|
||||
if (key == kKey::KeySpacebar && vr_active)
|
||||
const auto plan = pp::app::plan_app_key_down_dispatch(
|
||||
layout.get(main_id) != nullptr,
|
||||
key == kKey::KeySpacebar,
|
||||
vr_active);
|
||||
if (plan.sync_vr_camera_rotation)
|
||||
canvas->m_canvas->m_cam_rot = vr_rot;
|
||||
redraw = true;
|
||||
keys[(int)key] = true;
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyDown;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_up(kKey key)
|
||||
{
|
||||
redraw = true;
|
||||
keys[(int)key] = false;
|
||||
const auto plan = pp::app::plan_app_key_up_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
keys[(int)key] = plan.set_key_down;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyUp;
|
||||
e.m_key = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
bool App::key_char(char key)
|
||||
{
|
||||
redraw = true;
|
||||
const auto plan = pp::app::plan_app_main_input_dispatch(layout.get(main_id) != nullptr);
|
||||
redraw = plan.request_redraw;
|
||||
KeyEvent e;
|
||||
e.m_type = kEventType::KeyChar;
|
||||
e.m_char = key;
|
||||
kEventResult ret = kEventResult::Available;
|
||||
if (auto* main = layout[main_id])
|
||||
if (auto* main = layout[main_id]; plan.dispatch_main && main)
|
||||
ret = main->on_event(&e);
|
||||
return ret == kEventResult::Consumed;
|
||||
}
|
||||
|
||||
void App::toggle_ui()
|
||||
{
|
||||
auto m = layout[main_id]->m_children[1];
|
||||
ui_visible = !ui_visible;
|
||||
for (int i = 1; i < m->m_children.size(); i++)
|
||||
m->m_children[i]->m_display = ui_visible;
|
||||
auto* main = layout[main_id];
|
||||
const std::size_t main_child_count = main ? main->m_children.size() : 0U;
|
||||
auto* panel_container = main_child_count > 1U ? main->m_children[1].get() : nullptr;
|
||||
const auto plan = pp::app::plan_app_ui_visibility_toggle(
|
||||
ui_visible,
|
||||
main != nullptr,
|
||||
main_child_count,
|
||||
panel_container ? panel_container->m_children.size() : 0U);
|
||||
if (!plan) {
|
||||
LOG("UI toggle plan failed: %s", plan.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_visible = plan.value().next_ui_visible;
|
||||
if (!panel_container)
|
||||
return;
|
||||
|
||||
for (std::size_t i = plan.value().first_panel_child_index;
|
||||
i < plan.value().panel_child_count;
|
||||
++i) {
|
||||
panel_container->m_children[i]->m_display = ui_visible;
|
||||
}
|
||||
}
|
||||
|
||||
void App::set_stylus()
|
||||
{
|
||||
has_stylus = true;
|
||||
if (canvas)
|
||||
const auto plan = pp::app::plan_app_stylus_attach(canvas != nullptr);
|
||||
has_stylus = plan.set_has_stylus;
|
||||
if (plan.enable_canvas_touch_lock && canvas)
|
||||
canvas->m_canvas->m_touch_lock = true;
|
||||
}
|
||||
|
||||
1399
src/app_layout.cpp
1399
src/app_layout.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,100 +1,68 @@
|
||||
#include "pch.h"
|
||||
#include "app.h"
|
||||
#include "legacy_gl_runtime_dispatch.h"
|
||||
#include "renderer_api/shader_catalog.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "shader.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_shader_manager_feature_state(pp::renderer::gl::OpenGlFeatureState feature_state) noexcept
|
||||
{
|
||||
ShaderManager::ext_framebuffer_fetch = feature_state.capabilities.framebuffer_fetch;
|
||||
ShaderManager::ext_map_aligned = feature_state.capabilities.map_buffer_alignment;
|
||||
ShaderManager::ext_float32 = feature_state.capabilities.float32_textures;
|
||||
ShaderManager::ext_float32_linear = feature_state.capabilities.float32_linear;
|
||||
ShaderManager::ext_float16 = feature_state.capabilities.float16_textures;
|
||||
ShaderManager::set_render_device_features(feature_state.features);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void App::initShaders()
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
if (!check_uniform_uniqueness())
|
||||
std::logic_error("check_uniform_uniqueness() failed");
|
||||
LOG("check_uniform_uniqueness() failed");
|
||||
#endif // _DEBUG
|
||||
|
||||
render_task([] {
|
||||
GLint n_exts;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
|
||||
for (int i = 0; i < n_exts; i++)
|
||||
{
|
||||
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
|
||||
if (ext.find("shader_framebuffer_fetch") != std::string::npos)
|
||||
ShaderManager::ext_framebuffer_fetch = true;
|
||||
if (ext.find("map_buffer_alignment") != std::string::npos)
|
||||
ShaderManager::ext_map_aligned = true;
|
||||
#if __GLES__ && !__WEB__
|
||||
if (ext.find("texture_float") != std::string::npos)
|
||||
ShaderManager::ext_float32 = true;
|
||||
if (ext.find("texture_float_linear") != std::string::npos)
|
||||
ShaderManager::ext_float32_linear = true;
|
||||
if (ext.find("color_buffer_float") != std::string::npos)
|
||||
ShaderManager::ext_float32 = true;
|
||||
if (ext.find("texture_half_float") != std::string::npos)
|
||||
ShaderManager::ext_float16 = true;
|
||||
if (ext.find("color_buffer_half_float") != std::string::npos)
|
||||
ShaderManager::ext_float16 = true;
|
||||
#endif
|
||||
LOG("EXT: %s", ext.c_str());
|
||||
const auto detection_result = pp::renderer::gl::query_opengl_capability_detection(
|
||||
pp::legacy::gl_runtime::extension_query_dispatch(),
|
||||
pp::renderer::gl::opengl_runtime_for_current_build());
|
||||
if (!detection_result.ok()) {
|
||||
LOG("OpenGL capability detection failed: %s", detection_result.status().message);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& detection = detection_result.value();
|
||||
for (const auto& extension : detection.extensions) {
|
||||
LOG("EXT: %s", extension.c_str());
|
||||
}
|
||||
|
||||
apply_shader_manager_feature_state(detection.feature_state);
|
||||
});
|
||||
|
||||
#if __GL__
|
||||
// In OpenGL 3.3 these should be already available
|
||||
ShaderManager::ext_float32_linear = true;
|
||||
ShaderManager::ext_float32 = true;
|
||||
ShaderManager::ext_float16 = true;
|
||||
#endif
|
||||
apply_shader_manager_feature_state(pp::renderer::gl::detect_opengl_feature_state(
|
||||
std::span<const std::string_view> {},
|
||||
pp::renderer::gl::opengl_runtime_for_current_build()));
|
||||
|
||||
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
|
||||
|
||||
LOG("initializing shaders");
|
||||
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl"))
|
||||
LOG("Failed to create shader Texture");
|
||||
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl"))
|
||||
LOG("Failed to create shader TextureAlpha");
|
||||
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl"))
|
||||
LOG("Failed to create shader TextureMask");
|
||||
if (!ShaderManager::load(kShader::TextureColorize, "data/shaders/texture-colorize.glsl"))
|
||||
LOG("Failed to create shader TextureColorize");
|
||||
if (!ShaderManager::load(kShader::TextureBlend, "data/shaders/texture-blend.glsl"))
|
||||
LOG("Failed to create shader TextureBlend");
|
||||
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl"))
|
||||
LOG("Failed to create shader StrokePreview");
|
||||
if (!ShaderManager::load(kShader::CompErase, "data/shaders/comp-erase.glsl"))
|
||||
LOG("Failed to create shader CompErase");
|
||||
if (!ShaderManager::load(kShader::CompDraw, "data/shaders/comp-draw.glsl"))
|
||||
LOG("Failed to create shader CompDraw");
|
||||
if (!ShaderManager::load(kShader::Color, "data/shaders/color.glsl"))
|
||||
LOG("Failed to create shader Color");
|
||||
if (!ShaderManager::load(kShader::ColorQuad, "data/shaders/color-quad.glsl"))
|
||||
LOG("Failed to create shader ColorQuad");
|
||||
if (!ShaderManager::load(kShader::ColorTri, "data/shaders/color-tri.glsl"))
|
||||
LOG("Failed to create shader ColorTri");
|
||||
if (!ShaderManager::load(kShader::ColorHue, "data/shaders/color-hue.glsl"))
|
||||
LOG("Failed to create shader ColorHue");
|
||||
if (!ShaderManager::load(kShader::UVs, "data/shaders/uvs.glsl"))
|
||||
LOG("Failed to create shader UVs");
|
||||
if (!ShaderManager::load(kShader::Font, "data/shaders/font.glsl"))
|
||||
LOG("Failed to create shader Font");
|
||||
if (!ShaderManager::load(kShader::Atlas, "data/shaders/atlas.glsl"))
|
||||
LOG("Failed to create shader Atlas");
|
||||
if (!ShaderManager::load(kShader::Stroke, "data/shaders/stroke.glsl"))
|
||||
LOG("Failed to create shader Stroke");
|
||||
if (!ShaderManager::load(kShader::StrokePad, "data/shaders/stroke-pad.glsl"))
|
||||
LOG("Failed to create shader StrokePad");
|
||||
if (!ShaderManager::load(kShader::StrokeDilate, "data/shaders/stroke-dilate.glsl"))
|
||||
LOG("Failed to create shader StrokeDilate");
|
||||
if (!ShaderManager::load(kShader::Checkerboard, "data/shaders/checkerboard.glsl"))
|
||||
LOG("Failed to create shader Checkerboard");
|
||||
if (!ShaderManager::load(kShader::Equirect, "data/shaders/equirect.glsl"))
|
||||
LOG("Failed to create shader Equirect");
|
||||
if (!ShaderManager::load(kShader::BrushStroke, "data/shaders/stroke-instanced.glsl"))
|
||||
LOG("Failed to create shader BrushStroke");
|
||||
if (!ShaderManager::load(kShader::VertexColor, "data/shaders/vertex-color.glsl"))
|
||||
LOG("Failed to create shader VertexColor");
|
||||
if (!ShaderManager::load(kShader::Lambert, "data/shaders/lambert.glsl"))
|
||||
LOG("Failed to create shader Lambert");
|
||||
if (!ShaderManager::load(kShader::LambertLightmap, "data/shaders/lightmap.glsl"))
|
||||
LOG("Failed to create shader LambertLightmap");
|
||||
if (!ShaderManager::load(kShader::BakeUV, "data/shaders/bake-uv.glsl"))
|
||||
LOG("Failed to create shader BakeUV");
|
||||
const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
|
||||
const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
|
||||
if (!catalog_status.ok())
|
||||
{
|
||||
LOG("Shader catalog validation failed: %s", catalog_status.message);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& shader : shader_catalog)
|
||||
{
|
||||
if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
|
||||
LOG("Failed to create shader %s", shader.name);
|
||||
}
|
||||
LOG("shaders initialized");
|
||||
}
|
||||
|
||||
|
||||
169
src/app_vr.cpp
169
src/app_vr.cpp
@@ -1,12 +1,88 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "app.h"
|
||||
#include "legacy_ui_gl_dispatch.h"
|
||||
#include "node_panel_grid.h"
|
||||
#include "util.h"
|
||||
#include "shape.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
bool win32_vr_start();
|
||||
void win32_vr_stop();
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
void set_active_texture_unit(std::uint32_t unit_index)
|
||||
{
|
||||
pp::legacy::ui_gl::activate_texture_unit(unit_index, "OpenGL VR");
|
||||
}
|
||||
|
||||
void unbind_texture_2d()
|
||||
{
|
||||
pp::legacy::ui_gl::unbind_texture_2d("OpenGL VR");
|
||||
}
|
||||
|
||||
void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
|
||||
{
|
||||
const auto status = pp::renderer::gl::apply_opengl_viewport(
|
||||
viewport,
|
||||
pp::renderer::gl::OpenGlViewportDispatch {
|
||||
.viewport = pp::legacy::ui_gl::set_opengl_viewport,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR UI viewport failed: %s", status.message);
|
||||
}
|
||||
|
||||
void apply_vr_ui_scissor_test(bool enabled)
|
||||
{
|
||||
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
|
||||
enabled,
|
||||
pp::renderer::gl::OpenGlScissorTestDispatch {
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR UI scissor test failed: %s", status.message);
|
||||
}
|
||||
|
||||
void apply_vr_render_capability(std::uint32_t state, bool enabled)
|
||||
{
|
||||
const auto status = pp::renderer::gl::apply_opengl_capability(
|
||||
state,
|
||||
enabled,
|
||||
pp::renderer::gl::OpenGlCapabilityDispatch {
|
||||
.enable = pp::legacy::ui_gl::enable_opengl_state,
|
||||
.disable = pp::legacy::ui_gl::disable_opengl_state,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR render state failed: %s", status.message);
|
||||
}
|
||||
|
||||
bool query_vr_render_capability(std::uint32_t state)
|
||||
{
|
||||
const auto result = pp::renderer::gl::query_opengl_capability_state(
|
||||
state,
|
||||
pp::renderer::gl::OpenGlCapabilityStateQueryDispatch {
|
||||
.is_enabled = pp::legacy::ui_gl::is_opengl_state_enabled,
|
||||
});
|
||||
if (!result.ok()) {
|
||||
LOG("OpenGL VR render state query failed: %s", result.status().message);
|
||||
return false;
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
void clear_vr_depth_buffer()
|
||||
{
|
||||
const auto status = pp::renderer::gl::clear_opengl_buffers(
|
||||
pp::renderer::gl::framebuffer_depth_buffer_mask(),
|
||||
pp::renderer::gl::OpenGlBufferClearDispatch {
|
||||
.clear = pp::legacy::ui_gl::clear_opengl_buffer,
|
||||
});
|
||||
if (!status.ok())
|
||||
LOG("OpenGL VR depth clear failed: %s", status.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool trigger_down = false;
|
||||
cbuffer<glm::vec3> controller_points(10);
|
||||
@@ -19,31 +95,28 @@ Sphere controller_ray;
|
||||
|
||||
bool App::vr_start()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return win32_vr_start();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
return start_platform_vr_mode();
|
||||
}
|
||||
|
||||
void App::vr_stop()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
win32_vr_stop();
|
||||
#endif
|
||||
stop_platform_vr_mode();
|
||||
}
|
||||
|
||||
void App::vr_draw_ui()
|
||||
{
|
||||
uirtt.bindFramebuffer();
|
||||
uirtt.clear();
|
||||
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect {
|
||||
.width = static_cast<std::int32_t>(uirtt.getWidth()),
|
||||
.height = static_cast<std::int32_t>(uirtt.getHeight()),
|
||||
});
|
||||
apply_vr_ui_scissor_test(true);
|
||||
auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);
|
||||
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
|
||||
layout[main_id]->m_children[i]->watch(observer);
|
||||
//msgbox->watch(observer);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
apply_vr_ui_scissor_test(false);
|
||||
uirtt.unbindFramebuffer();
|
||||
}
|
||||
|
||||
@@ -185,12 +258,12 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::vec3 origin = glm::vec3(0, 0, -1) * glm::transpose(glm::mat3(pose));
|
||||
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
|
||||
|
||||
auto blend = glIsEnabled(GL_BLEND);
|
||||
auto depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
const bool blend = query_vr_render_capability(pp::renderer::gl::blend_state());
|
||||
const bool depth = query_vr_render_capability(pp::renderer::gl::depth_test_state());
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||
clear_vr_depth_buffer();
|
||||
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
{
|
||||
@@ -205,9 +278,9 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
m_face_plane.draw_fill();
|
||||
}
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), true);
|
||||
clear_vr_depth_buffer();
|
||||
|
||||
for (size_t i = 0; i < canvas->m_canvas->m_layers.size(); i++)
|
||||
{
|
||||
@@ -241,17 +314,17 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
|
||||
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
}
|
||||
else if (canvas->m_canvas->m_show_tmp && canvas->m_canvas->m_current_layer_idx == layer_index)
|
||||
@@ -292,28 +365,28 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
|
||||
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
canvas->m_canvas->m_tmp[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
canvas->m_canvas->m_tmp_dual[plane_index].bindTexture();
|
||||
glActiveTexture(GL_TEXTURE4);
|
||||
set_active_texture_unit(4);
|
||||
b->m_pattern_texture ?
|
||||
b->m_pattern_texture->bind() :
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
unbind_texture_2d();
|
||||
m_face_plane.draw_fill();
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
set_active_texture_unit(3);
|
||||
if (b->m_dual_enabled)
|
||||
canvas->m_canvas->m_tmp_dual[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
set_active_texture_unit(2);
|
||||
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
set_active_texture_unit(1);
|
||||
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
}
|
||||
else
|
||||
@@ -325,7 +398,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
|
||||
@@ -352,7 +425,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
m_face_plane.draw_stroke();
|
||||
}
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
|
||||
// draw the brush
|
||||
/*
|
||||
auto mode = dynamic_cast<CanvasModePen*>(canvas->m_canvas->modes[(int)canvas->m_canvas->m_current_mode][0]);
|
||||
@@ -377,8 +450,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size / height)) *
|
||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||
);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||
tex.bind();
|
||||
sampler_linear.bind(0);
|
||||
@@ -399,7 +472,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
ShaderManager::use(kShader::Texture);
|
||||
ShaderManager::u_int(kShaderUniform::Tex, 0);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
set_active_texture_unit(0);
|
||||
uirtt.bindTexture();
|
||||
m_face_plane.draw_fill();
|
||||
uirtt.unbindTexture();
|
||||
@@ -451,8 +524,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
|
||||
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
|
||||
);
|
||||
glEnable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
|
||||
set_active_texture_unit(0);
|
||||
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
|
||||
tex.bind();
|
||||
sampler_linear.bind(0);
|
||||
@@ -466,7 +539,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
for (auto& mode : *canvas->m_canvas->m_mode)
|
||||
mode->on_Draw(ortho_proj, proj, camera);
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(pp::renderer::gl::depth_test_state());
|
||||
if (canvas->m_canvas->m_smask_active)
|
||||
{
|
||||
canvas->m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
|
||||
@@ -479,8 +552,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
|
||||
mode->on_Draw(ortho_proj, proj, camera);
|
||||
*/
|
||||
|
||||
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
|
||||
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
|
||||
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend);
|
||||
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth);
|
||||
sampler.unbind();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "pch.h"
|
||||
#include "log.h"
|
||||
#include "asset.h"
|
||||
#include "platform_api/network_tls_policy.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <Foundation/Foundation.h>
|
||||
@@ -8,9 +9,24 @@
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/asset_manager.h>
|
||||
#include <dirent.h>
|
||||
AAssetManager* Asset::m_am;
|
||||
void* Asset::m_android_asset_manager;
|
||||
bool android_create_dir(const std::string& path);
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] AAsset* android_asset_handle(void* asset)
|
||||
{
|
||||
return static_cast<AAsset*>(asset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Asset::set_android_asset_manager(void* asset_manager)
|
||||
{
|
||||
m_android_asset_manager = asset_manager;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Asset::delete_file(const std::string& path)
|
||||
@@ -65,7 +81,7 @@ std::vector<std::string> Asset::list_files(std::string folder, const std::string
|
||||
#elif __ANDROID__
|
||||
if (is_asset)
|
||||
{
|
||||
AAssetDir* dir = AAssetManager_openDir(Asset::m_am, folder.c_str());
|
||||
AAssetDir* dir = AAssetManager_openDir(static_cast<AAssetManager*>(Asset::m_android_asset_manager), folder.c_str());
|
||||
while (const char* name = AAssetDir_getNextFileName(dir))
|
||||
{
|
||||
//LOG("asset: %s", name);
|
||||
@@ -187,9 +203,8 @@ bool Asset::open_url(const std::string& url, std::function<bool(float)> progress
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &tmp_data);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler_asset);
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
|
||||
#ifdef __ANDROID__
|
||||
if (pp::platform::default_disables_network_tls_verification())
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
if (progress)
|
||||
{
|
||||
on_progress = progress;
|
||||
@@ -237,7 +252,10 @@ bool Asset::open(const char* path)
|
||||
#ifdef __ANDROID__
|
||||
if (is_asset(path))
|
||||
{
|
||||
if (!(m_asset = AAssetManager_open(m_am, path, AASSET_MODE_RANDOM)))
|
||||
if (!(m_android_asset = AAssetManager_open(
|
||||
static_cast<AAssetManager*>(m_android_asset_manager),
|
||||
path,
|
||||
AASSET_MODE_RANDOM)))
|
||||
{
|
||||
LOG("AAssetManager_open failed %s", path);
|
||||
return false;
|
||||
@@ -283,8 +301,8 @@ glm::uint8_t* Asset::read_all()
|
||||
{
|
||||
if (is_asset(m_current_path))
|
||||
{
|
||||
m_len = (int)AAsset_getLength(m_asset);
|
||||
m_data = (uint8_t*)AAsset_getBuffer(m_asset);
|
||||
m_len = (int)AAsset_getLength(android_asset_handle(m_android_asset));
|
||||
m_data = (uint8_t*)AAsset_getBuffer(android_asset_handle(m_android_asset));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,9 +334,9 @@ glm::uint8_t* Asset::read_all()
|
||||
void Asset::close()
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
if (m_asset)
|
||||
AAsset_close(m_asset);
|
||||
m_asset = nullptr;
|
||||
if (m_android_asset)
|
||||
AAsset_close(android_asset_handle(m_android_asset));
|
||||
m_android_asset = nullptr;
|
||||
#else
|
||||
if (m_fp)
|
||||
fclose(m_fp);
|
||||
|
||||
@@ -4,8 +4,13 @@ class Asset
|
||||
{
|
||||
public:
|
||||
#ifdef __ANDROID__
|
||||
static AAssetManager* m_am;
|
||||
AAsset* m_asset = nullptr;
|
||||
static void set_android_asset_manager(void* asset_manager);
|
||||
|
||||
private:
|
||||
static void* m_android_asset_manager;
|
||||
void* m_android_asset = nullptr;
|
||||
|
||||
public:
|
||||
#endif
|
||||
static std::vector<std::string> list_files(std::string folder, const std::string& filter_regex);
|
||||
static bool exist(std::string path);
|
||||
|
||||
160
src/assets/brush_package.cpp
Normal file
160
src/assets/brush_package.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "assets/brush_package.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::uint16_t read_u16_le(std::span<const std::byte> bytes, std::size_t offset) noexcept
|
||||
{
|
||||
const auto lo = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset]));
|
||||
const auto hi = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset + 1U]));
|
||||
return static_cast<std::uint16_t>(lo | static_cast<std::uint16_t>(hi << 8U));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_word_extension(std::string_view value) noexcept
|
||||
{
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const char raw : value) {
|
||||
const auto ch = static_cast<unsigned char>(raw);
|
||||
if (std::isalnum(ch) == 0 && ch != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept
|
||||
{
|
||||
if (magic != "PPBR") {
|
||||
return pp::foundation::Status::invalid_argument("PPBR header magic is invalid");
|
||||
}
|
||||
|
||||
// DEBT-0049: preserve legacy version acceptance until PPBR compatibility fixtures exist.
|
||||
if (major != ppbr_legacy_major_version && minor != ppbr_legacy_minor_version) {
|
||||
return pp::foundation::Status::invalid_argument("PPBR version is unsupported");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrHeader> parse_ppbr_header(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < ppbr_header_size) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(
|
||||
pp::foundation::Status::out_of_range("PPBR header is truncated"));
|
||||
}
|
||||
|
||||
const std::string_view magic(reinterpret_cast<const char*>(bytes.data()), 4U);
|
||||
const auto major = read_u16_le(bytes, 4U);
|
||||
const auto minor = read_u16_le(bytes, 6U);
|
||||
const auto status = validate_ppbr_header(magic, major, minor);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpbrHeader>::success(PpbrHeader {
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::string> normalize_ppbr_export_path(std::string_view requested_path)
|
||||
{
|
||||
if (requested_path.empty()) {
|
||||
return pp::foundation::Result<std::string>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must not be empty"));
|
||||
}
|
||||
|
||||
std::string path(requested_path);
|
||||
if (requested_path.find(".ppbr") == std::string_view::npos) {
|
||||
path += ".ppbr";
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::string>::success(std::move(path));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy)
|
||||
{
|
||||
const auto normalized = normalize_ppbr_export_path(requested_path);
|
||||
if (!normalized) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(normalized.status());
|
||||
}
|
||||
|
||||
const auto slash = normalized.value().find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a directory and file name"));
|
||||
}
|
||||
|
||||
const auto dot = normalized.value().find_last_of('.');
|
||||
if (dot == std::string::npos || dot <= slash + 1U || dot + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a file extension"));
|
||||
}
|
||||
|
||||
PpbrExportPaths paths;
|
||||
paths.package_path = normalized.value();
|
||||
paths.directory = normalized.value().substr(0, slash);
|
||||
paths.stem = normalized.value().substr(slash + 1U, dot - slash - 1U);
|
||||
paths.extension = normalized.value().substr(dot + 1U);
|
||||
if (!is_word_extension(paths.extension)) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path extension contains unsupported characters"));
|
||||
}
|
||||
|
||||
if (data_directory_policy == PpbrDataDirectoryPolicy::override_directory) {
|
||||
paths.data_directory = std::string(override_data_directory) + "/" + paths.stem + "_data";
|
||||
} else {
|
||||
paths.data_directory = paths.directory + "/" + paths.stem + "_data";
|
||||
}
|
||||
paths.data_directory_enabled = export_data && !paths.data_directory.empty();
|
||||
|
||||
return pp::foundation::Result<PpbrExportPaths>::success(std::move(paths));
|
||||
}
|
||||
|
||||
pp::foundation::Result<BrushPackageImageTargetPaths> plan_brush_package_image_target_paths(
|
||||
std::string_view data_path,
|
||||
BrushPackageImageKind kind,
|
||||
std::string_view image_name,
|
||||
std::string_view image_extension)
|
||||
{
|
||||
if (data_path.empty()) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package data path must not be empty"));
|
||||
}
|
||||
if (image_name.empty()) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package image name must not be empty"));
|
||||
}
|
||||
if (!is_word_extension(image_extension)) {
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("brush package image extension contains unsupported characters"));
|
||||
}
|
||||
|
||||
const auto directory = kind == BrushPackageImageKind::brush_tip ? "brushes" : "patterns";
|
||||
const std::string base_path = std::string(data_path) + "/" + directory + "/" + std::string(image_name)
|
||||
+ "." + std::string(image_extension);
|
||||
|
||||
return pp::foundation::Result<BrushPackageImageTargetPaths>::success(BrushPackageImageTargetPaths {
|
||||
.image_path = base_path,
|
||||
.thumbnail_path = std::string(data_path) + "/" + directory + "/thumbs/" + std::string(image_name)
|
||||
+ "." + std::string(image_extension),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pp::assets
|
||||
69
src/assets/brush_package.h
Normal file
69
src/assets/brush_package.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppbr_header_size = 8;
|
||||
constexpr std::uint16_t ppbr_legacy_major_version = 0;
|
||||
constexpr std::uint16_t ppbr_legacy_minor_version = 1;
|
||||
|
||||
enum class PpbrDataDirectoryPolicy {
|
||||
next_to_package,
|
||||
override_directory,
|
||||
};
|
||||
|
||||
enum class BrushPackageImageKind {
|
||||
brush_tip,
|
||||
pattern,
|
||||
};
|
||||
|
||||
struct PpbrHeader {
|
||||
std::uint16_t major = 0;
|
||||
std::uint16_t minor = 0;
|
||||
};
|
||||
|
||||
struct BrushPackageImageTargetPaths {
|
||||
std::string image_path;
|
||||
std::string thumbnail_path;
|
||||
};
|
||||
|
||||
struct PpbrExportPaths {
|
||||
std::string package_path;
|
||||
std::string directory;
|
||||
std::string stem;
|
||||
std::string extension;
|
||||
std::string data_directory;
|
||||
bool data_directory_enabled = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrHeader> parse_ppbr_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::string> normalize_ppbr_export_path(
|
||||
std::string_view requested_path);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<BrushPackageImageTargetPaths> plan_brush_package_image_target_paths(
|
||||
std::string_view data_path,
|
||||
BrushPackageImageKind kind,
|
||||
std::string_view image_name,
|
||||
std::string_view image_extension);
|
||||
|
||||
} // namespace pp::assets
|
||||
323
src/assets/image_pixels.cpp
Normal file
323
src/assets/image_pixels.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
void append_encoded_bytes(void* context, void* data, int size)
|
||||
{
|
||||
append_png_bytes(context, data, size);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool has_jpeg_soi(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
return bytes.size() >= 2U
|
||||
&& bytes[0] == std::byte { 0xff }
|
||||
&& bytes[1] == std::byte { 0xd8 };
|
||||
}
|
||||
|
||||
constexpr char gpano_xmp[] =
|
||||
"http://ns.adobe.com/xap/1.0/\0"
|
||||
R"(<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" xmptk="SAMSUNG 360CAM">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about="" xmlns:GPano="http://ns.google.com/photos/1.0/panorama/">
|
||||
<GPano:ProjectionType>equirectangular</GPano:ProjectionType>
|
||||
<GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>
|
||||
<GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>
|
||||
<GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>
|
||||
<GPano:PoseHeadingDegrees>0</GPano:PoseHeadingDegrees>
|
||||
<GPano:PosePitchDegrees>0</GPano:PosePitchDegrees>
|
||||
<GPano:PoseRollDegrees>0</GPano:PoseRollDegrees>
|
||||
<GPano:StitchingSoftware>PanoPainter</GPano:StitchingSoftware>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end="r"?>)";
|
||||
|
||||
}
|
||||
|
||||
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<Rgba8Image> decode_jpeg_rgba8(std::span<const std::byte> bytes)
|
||||
{
|
||||
if (!has_jpeg_soi(bytes)) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::invalid_argument("JPEG signature is invalid"));
|
||||
}
|
||||
|
||||
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("JPEG 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("JPEG 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) > max_image_dimension
|
||||
|| static_cast<std::uint32_t>(height) > max_image_dimension) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::out_of_range("decoded JPEG dimensions are outside the configured range"));
|
||||
}
|
||||
|
||||
const auto byte_count = rgba_byte_size(
|
||||
static_cast<std::uint32_t>(width),
|
||||
static_cast<std::uint32_t>(height));
|
||||
if (!byte_count) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(byte_count.status());
|
||||
}
|
||||
|
||||
Rgba8Image image {
|
||||
.width = static_cast<std::uint32_t>(width),
|
||||
.height = static_cast<std::uint32_t>(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));
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::vector<std::byte>> encode_jpeg_rgba8(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::span<const std::uint8_t> pixels,
|
||||
int quality)
|
||||
{
|
||||
if (width == 0 || height == 0) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::invalid_argument("JPEG 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("JPEG dimensions exceed encoder limits"));
|
||||
}
|
||||
|
||||
if (quality < 1 || quality > 100) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::out_of_range("JPEG quality must be within 1..100"));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
std::vector<std::byte> encoded;
|
||||
const auto result = stbi_write_jpg_to_func(
|
||||
append_encoded_bytes,
|
||||
&encoded,
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
4,
|
||||
pixels.data(),
|
||||
quality);
|
||||
|
||||
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 JPEG"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(encoded));
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::vector<std::byte>> inject_gpano_xmp_into_jpeg(std::span<const std::byte> jpeg)
|
||||
{
|
||||
if (!has_jpeg_soi(jpeg)) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::invalid_argument("JPEG signature is invalid"));
|
||||
}
|
||||
|
||||
constexpr auto xmp_size = sizeof(gpano_xmp);
|
||||
static_assert(xmp_size + 2U <= 0xffffU);
|
||||
const auto segment_length = static_cast<std::uint16_t>(xmp_size + 2U);
|
||||
|
||||
std::vector<std::byte> encoded;
|
||||
encoded.reserve(jpeg.size() + xmp_size + 4U);
|
||||
encoded.insert(encoded.end(), jpeg.begin(), jpeg.begin() + 2);
|
||||
encoded.push_back(std::byte { 0xff });
|
||||
encoded.push_back(std::byte { 0xe1 });
|
||||
encoded.push_back(static_cast<std::byte>((segment_length >> 8U) & 0xffU));
|
||||
encoded.push_back(static_cast<std::byte>(segment_length & 0xffU));
|
||||
const auto* xmp_begin = reinterpret_cast<const std::byte*>(gpano_xmp);
|
||||
encoded.insert(encoded.end(), xmp_begin, xmp_begin + xmp_size);
|
||||
encoded.insert(encoded.end(), jpeg.begin() + 2, jpeg.end());
|
||||
|
||||
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(encoded));
|
||||
}
|
||||
|
||||
}
|
||||
38
src/assets/image_pixels.h
Normal file
38
src/assets/image_pixels.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#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<Rgba8Image> decode_jpeg_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);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> encode_jpeg_rgba8(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::span<const std::uint8_t> pixels,
|
||||
int quality);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> inject_gpano_xmp_into_jpeg(
|
||||
std::span<const std::byte> jpeg);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
#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 {
|
||||
@@ -11,6 +20,125 @@ namespace {
|
||||
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
|
||||
@@ -70,4 +198,691 @@ pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
#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;
|
||||
@@ -34,7 +44,156 @@ struct PpiHeaderInfo {
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
1088
src/canvas.cpp
1088
src/canvas.cpp
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,6 @@
|
||||
#include <stack>
|
||||
#include "mp4enc.h"
|
||||
|
||||
#if __WEB__
|
||||
#define CANVAS_RES 512
|
||||
#else
|
||||
#define CANVAS_RES 1536
|
||||
#endif
|
||||
|
||||
struct PPIThumb
|
||||
{
|
||||
int width = 128;
|
||||
@@ -205,7 +199,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();
|
||||
|
||||
@@ -36,9 +36,12 @@ void ActionStroke::undo()
|
||||
{
|
||||
App::I->render_task([&]
|
||||
{
|
||||
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());
|
||||
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).unbindTexture();
|
||||
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).updateRgba8(
|
||||
(int)m_box[i].x,
|
||||
(int)m_box[i].y,
|
||||
(int)box_sz.x,
|
||||
(int)box_sz.y,
|
||||
m_image[i].get());
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -76,13 +79,14 @@ 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);
|
||||
App::I->render_task([&]
|
||||
{
|
||||
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());
|
||||
layer->rtt(i, m_frame_idx).unbindFramebuffer();
|
||||
});
|
||||
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);
|
||||
layer->rtt(i, m_frame_idx).readPixelsRgba8(
|
||||
static_cast<int>(box_or.x),
|
||||
static_cast<int>(box_or.y),
|
||||
static_cast<int>(box_sz.x),
|
||||
static_cast<int>(box_sz.y),
|
||||
action->m_image[i].get());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,10 +1,74 @@
|
||||
#include "pch.h"
|
||||
#include "canvas_layer.h"
|
||||
#include "app.h"
|
||||
#include "legacy_ui_gl_dispatch.h"
|
||||
#include "log.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "rtt.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
uint32_t Layer::s_count = 0;
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_layer_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
|
||||
{
|
||||
pp::legacy::ui_gl::apply_viewport(x, y, width, height, "Layer");
|
||||
}
|
||||
|
||||
void apply_layer_capability(std::uint32_t state, bool enabled)
|
||||
{
|
||||
pp::legacy::ui_gl::set_capability(state, enabled, "Layer");
|
||||
}
|
||||
|
||||
void set_layer_active_texture_unit(std::uint32_t unit_index)
|
||||
{
|
||||
pp::legacy::ui_gl::activate_texture_unit(unit_index, "Layer");
|
||||
}
|
||||
|
||||
void bind_layer_texture_cube(std::uint32_t texture_id)
|
||||
{
|
||||
pp::legacy::ui_gl::bind_texture_cube(texture_id, "Layer");
|
||||
}
|
||||
|
||||
void copy_layer_framebuffer_to_texture(
|
||||
std::uint32_t texture_target,
|
||||
std::int32_t destination_x,
|
||||
std::int32_t destination_y,
|
||||
std::int32_t source_x,
|
||||
std::int32_t source_y,
|
||||
std::int32_t width,
|
||||
std::int32_t height)
|
||||
{
|
||||
copy_framebuffer_to_texture_target(
|
||||
texture_target,
|
||||
destination_x,
|
||||
destination_y,
|
||||
source_x,
|
||||
source_y,
|
||||
width,
|
||||
height);
|
||||
}
|
||||
|
||||
void clear_layer_color_buffer(const glm::vec4& color)
|
||||
{
|
||||
pp::legacy::ui_gl::clear_color_buffer({ color.r, color.g, color.b, color.a }, "Layer");
|
||||
}
|
||||
|
||||
std::array<float, 4> query_layer_clear_color()
|
||||
{
|
||||
return pp::legacy::ui_gl::query_clear_color("Layer");
|
||||
}
|
||||
|
||||
void restore_layer_clear_color(std::array<float, 4> color)
|
||||
{
|
||||
pp::legacy::ui_gl::set_clear_color(color, "Layer");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RTT& Layer::rtt(int i, int frame /*= -1*/)
|
||||
{
|
||||
if (frame == -1)
|
||||
@@ -44,7 +108,7 @@ TextureCube Layer::gen_cube()
|
||||
{
|
||||
ret.bind();
|
||||
rtt(i).bindFramebuffer();
|
||||
glCopyTexSubImage2D(TextureCube::m_faces_map[i], 0, 0, 0, 0, 0, w, w);
|
||||
copy_layer_framebuffer_to_texture(pp::renderer::gl::cube_face_texture_target(i), 0, 0, 0, 0, w, w);
|
||||
rtt(i).unbindFramebuffer();
|
||||
});
|
||||
}
|
||||
@@ -70,16 +134,16 @@ 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);
|
||||
apply_layer_capability(pp::renderer::gl::blend_state(), false);
|
||||
|
||||
latlong.bindFramebuffer();
|
||||
|
||||
//latlong.clear({ 0, 1, 1, 1 });
|
||||
|
||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
apply_layer_viewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
||||
set_layer_active_texture_unit(0U);
|
||||
bind_layer_texture_cube(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 +152,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());
|
||||
copy_framebuffer_to_texture_2d(0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
|
||||
latlong.unbindFramebuffer();
|
||||
|
||||
@@ -115,13 +179,13 @@ PBO Layer::gen_equirect_pbo(glm::ivec2 size /*= { 0, 0 }*/)
|
||||
|
||||
App::I->render_task([&]
|
||||
{
|
||||
glDisable(GL_BLEND);
|
||||
apply_layer_capability(pp::renderer::gl::blend_state(), false);
|
||||
|
||||
latlong.bindFramebuffer();
|
||||
|
||||
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
|
||||
apply_layer_viewport(0, 0, latlong.getWidth(), latlong.getHeight());
|
||||
set_layer_active_texture_unit(0U);
|
||||
bind_layer_texture_cube(cube.m_cubetex_id);
|
||||
|
||||
ShaderManager::use(kShader::Equirect);
|
||||
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
|
||||
@@ -457,16 +521,14 @@ void LayerFrame::clear(const glm::vec4& c)
|
||||
App::I->render_task([&]
|
||||
{
|
||||
// push clear color state
|
||||
GLfloat cc[4];
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
||||
glClearColor(c.r, c.g, c.b, c.a);
|
||||
const auto cc = query_layer_clear_color();
|
||||
|
||||
bool erase = (c.a == 0.f);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
m_rtt[i].bindFramebuffer();
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
clear_layer_color_buffer(c);
|
||||
m_rtt[i].unbindFramebuffer();
|
||||
|
||||
if (erase)
|
||||
@@ -482,7 +544,7 @@ void LayerFrame::clear(const glm::vec4& c)
|
||||
}
|
||||
|
||||
// restore clear color state
|
||||
glClearColor(cc[0], cc[1], cc[2], cc[3]);
|
||||
restore_layer_clear_color(cc);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -528,13 +590,11 @@ void LayerFrame::restore(const Snapshot& snap)
|
||||
// it's just a quick fix DON'T SHIP!!
|
||||
//m_rtt[i].recreate();
|
||||
|
||||
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,
|
||||
m_rtt[i].updateRgba8(
|
||||
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),
|
||||
snap.image[i].get());
|
||||
m_rtt[i].unbindTexture();
|
||||
LOG("restore face %d - %d bytes (%dx%d)", i,
|
||||
(int)box_sz.x * (int)box_sz.y * 4, (int)box_sz.x, (int)box_sz.y);
|
||||
}
|
||||
@@ -558,11 +618,12 @@ LayerFrame::Snapshot LayerFrame::snapshot(std::array<glm::vec4, 6>* dirty_box /*
|
||||
|
||||
snap.image[i] = std::make_unique<uint8_t[]>(m_rtt[i].bytes());
|
||||
|
||||
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());
|
||||
m_rtt[i].unbindFramebuffer();
|
||||
m_rtt[i].readPixelsRgba8(
|
||||
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),
|
||||
snap.image[i].get());
|
||||
}
|
||||
});
|
||||
return snap;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user