From f6f5e5f2886d8d0a5c9e2208e9e37a0396aafc4a Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Mon, 19 Oct 2026 22:05:01 +0200 Subject: [PATCH] Fry3DX 0.5: builds for Windows, Linux and Android, a phone layout, Apache 2.0 - Apache License 2.0 - Windows: cross-built with clang-cl (cmake --preset windows), a window program with its icon; Linux: a portable preset (SDL and the C++ library inside); Android: android/build_apk.py makes the APK - tools/package.py packs the downloads; a GitHub workflow builds all three and makes a release for a v* tag - A phone layout (fry_touch, on by default on Android): the UI drawn bigger, panels when asked for, a bar of big buttons, one finger to turn the view, two to move and pinch, a long press for a right-click; files through the phone's file picker - Settings and autosaves now in a Nexoniarz folder (copied over once) - README rewritten, the website shows the downloads and the phone Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP --- .github/workflows/build.yml | 89 ++++++ .gitignore | 1 + CMakeLists.txt | 18 +- CMakePresets.json | 12 +- LICENSE | 202 +++++++++++++ README.md | 205 +++++++------ android/AndroidManifest.xml | 30 ++ android/build_apk.py | 116 +++++++ .../github/nexoniarz/fry3dx/FryActivity.java | 11 + android/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5955 bytes android/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3642 bytes android/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 8224 bytes android/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 13392 bytes android/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 17228 bytes android/shell.nix | 20 ++ branding/fry3dx.ico | Bin 0 -> 37956 bytes cmake/Dependencies.cmake | 10 +- cmake/toolchains/clang-cl-xwin | 14 + cmake/toolchains/lld-link-xwin | 6 + cmake/toolchains/make-case-overlay.py | 26 ++ cmake/toolchains/windows-msvc.cmake | 30 ++ content/{keel => engine}/cfg/default.cfg | 0 content/{keel => engine}/skies/CREDITS.txt | 0 content/{keel => engine}/skies/day.ksky | Bin docs/content.js | 18 +- docs/img/phone.webp | Bin 0 -> 43742 bytes docs/index.html | 3 +- docs/site.js | 8 + docs/style.css | 5 + engine/engine.cpp | 18 +- engine/rhi/gl/gl_backend.cpp | 2 + engine/rhi/gl/gl_legacy.cpp | 2 + engine/rhi/vulkan/vk_backend.cpp | 2 + engine/ui/ui.cpp | 24 ++ engine/ui/ui.h | 6 + shell.nix | 5 + src/CMakeLists.txt | 18 +- src/app.cpp | 283 ++++++++++++++++-- src/fry3dx.rc | 25 ++ tools/package.py | 113 +++++++ 40 files changed, 1172 insertions(+), 150 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 LICENSE create mode 100644 android/AndroidManifest.xml create mode 100644 android/build_apk.py create mode 100644 android/java/io/github/nexoniarz/fry3dx/FryActivity.java create mode 100644 android/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/shell.nix create mode 100644 branding/fry3dx.ico create mode 100755 cmake/toolchains/clang-cl-xwin create mode 100755 cmake/toolchains/lld-link-xwin create mode 100755 cmake/toolchains/make-case-overlay.py create mode 100644 cmake/toolchains/windows-msvc.cmake rename content/{keel => engine}/cfg/default.cfg (100%) rename content/{keel => engine}/skies/CREDITS.txt (100%) rename content/{keel => engine}/skies/day.ksky (100%) create mode 100644 docs/img/phone.webp create mode 100644 src/fry3dx.rc create mode 100644 tools/package.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..44a50eb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,89 @@ +# Builds Fry3DX for Linux (a portable .tar.gz and an AppImage), Windows (.zip) and Android (.apk). +# A tag v* (git tag v0.4.0 && git push --tags) also makes a GitHub release with them; +# otherwise they're the run's artifacts (Actions tab). +# Android signing: secrets FRY_KEYSTORE_B64 (the keystore, base64) and FRY_KEYSTORE_PASS. Keep +# the same key: Android only updates an app signed with the key it was installed with. +name: Build +on: + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + linux: + # The oldest supported Ubuntu: what's built here runs on any distro as new (glibc 2.35+). + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Tools + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y gcc-13 g++-13 ninja-build libfuse2 \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxfixes-dev \ + libwayland-dev libwayland-bin wayland-protocols libxkbcommon-dev libegl-dev libdecor-0-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev libdbus-1-dev libudev-dev libdrm-dev libgbm-dev + pip install cmake==3.31.6 + wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool + chmod +x appimagetool + - name: Build + env: { CC: gcc-13, CXX: g++-13 } + run: | + cmake --preset portable -DGLSLC=$ANDROID_NDK_HOME/shader-tools/linux-x86_64/glslc + cmake --build build/portable + - name: Package + run: APPIMAGETOOL=$PWD/appimagetool python3 tools/package.py linux appimage + - uses: actions/upload-artifact@v4 + with: { name: linux, path: "dist/*" } + + windows: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + - name: Build + shell: bash + run: | + GLSLC=$(ls -d "$ANDROID_NDK_HOME"/shader-tools/windows-x86_64/glslc.exe) + cmake -S . -B build/windows -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl \ + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded "-DGLSLC=$GLSLC" + cmake --build build/windows + - name: Package + run: python tools/package.py windows + - uses: actions/upload-artifact@v4 + with: { name: windows, path: "dist/*" } + + android: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: { distribution: temurin, java-version: "17" } + - name: SDK + run: | + yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;27.2.12479018" "build-tools;35.0.0" "platforms;android-35" > /dev/null + sudo apt-get install -y ninja-build + - name: Build + env: + ANDROID_NDK_ROOT: ${{ env.ANDROID_HOME }}/ndk/27.2.12479018 + KEY_B64: ${{ secrets.FRY_KEYSTORE_B64 }} + FRY_KEYSTORE_PASS: ${{ secrets.FRY_KEYSTORE_PASS }} + run: | + export ANDROID_NDK_ROOT="$ANDROID_HOME/ndk/27.2.12479018" + if [ -n "$KEY_B64" ]; then echo "$KEY_B64" | base64 -d > "$RUNNER_TEMP/fry.keystore"; export FRY_KEYSTORE="$RUNNER_TEMP/fry.keystore"; fi + python3 android/build_apk.py + - uses: actions/upload-artifact@v4 + with: { name: android, path: "dist/*" } + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [linux, windows, android] + runs-on: ubuntu-latest + permissions: { contents: write } + steps: + - uses: actions/download-artifact@v4 + with: { path: dist, merge-multiple: true } + - uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 4234a9a..a1ee074 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ *.blend1 __pycache__/ *.fry~ +dist/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 06fc132..8413d35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,17 @@ cmake_minimum_required(VERSION 3.25) +project(Fry3DX VERSION 0.5.0 LANGUAGES C CXX) if(UNIX AND NOT APPLE) add_link_options(-rdynamic) # function names in crash reports endif() -project(Fry3DX VERSION 0.1.0 LANGUAGES C CXX) +# FRY_PORTABLE: a Linux build to hand out (runs on any distro as new as the one it's built on): +# SDL and the C++ library inside the program. +option(FRY_PORTABLE "Build SDL in and link the C++ runtime statically" OFF) +if(FRY_PORTABLE AND UNIX AND NOT APPLE AND NOT ANDROID) + add_link_options(-static-libstdc++ -static-libgcc) +endif() -# Fry3DX — Frying 3D Extreme: a 3D modelling app that's easy to pick up. Built on its own -# engine's window, renderer and UI (engine/). +# Fry3DX — Frying 3D Extreme: a 3D modelling app that's easy to pick up. Its engine (engine/): +# window, renderer and UI. find_program(FRY_CCACHE ccache) if(FRY_CCACHE AND NOT CMAKE_CXX_COMPILER_LAUNCHER) @@ -29,12 +35,12 @@ include(cmake/Shaders.cmake) add_subdirectory(engine) add_subdirectory(src) -# Content next to the program: bin/keel/ (the engine's: fonts, sky, config); bin/Fry3DX/ (the app's own, none yet). +# Content next to the program: bin/engine/ (the engine's: fonts, sky, config); bin/Fry3DX/ (the app's own, none yet). add_custom_target(content ALL COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/content ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/keel/fonts ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Fry3DX + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/engine/fonts ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Fry3DX COMMAND ${CMAKE_COMMAND} -E copy_if_different ${dejavu_SOURCE_DIR}/ttf/DejaVuSans.ttf ${dejavu_SOURCE_DIR}/ttf/DejaVuSans-Bold.ttf - ${dejavu_SOURCE_DIR}/LICENSE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/keel/fonts/ + ${dejavu_SOURCE_DIR}/LICENSE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/engine/fonts/ COMMENT "Copying content" VERBATIM) enable_testing() diff --git a/CMakePresets.json b/CMakePresets.json index a1dbb08..54c2b77 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -3,10 +3,18 @@ "configurePresets": [ { "name": "base", "hidden": true, "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}" }, { "name": "debug", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, - { "name": "release", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } } + { "name": "release", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }, + { "name": "portable", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FRY_PORTABLE": "ON" } }, + { "name": "windows", "inherits": "base", "toolchainFile": "${sourceDir}/cmake/toolchains/windows-msvc.cmake", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded" } }, + { "name": "android", "inherits": "base", "toolchainFile": "$env{ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "ANDROID_ABI": "arm64-v8a", "ANDROID_PLATFORM": "android-28", "ANDROID_STL": "c++_static" } } ], "buildPresets": [ { "name": "debug", "configurePreset": "debug" }, - { "name": "release", "configurePreset": "release" } + { "name": "release", "configurePreset": "release" }, + { "name": "portable", "configurePreset": "portable" }, + { "name": "windows", "configurePreset": "windows" }, + { "name": "android", "configurePreset": "android" } ] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index d5907ef..bd88509 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ -# Fry3DX — Frying 3D Extreme +

-**A 3D modeller that does what you'd expect: pick a tool, click, drag.** -Made by [Nexoniarz](https://github.com/Nexoniarz) · Website: https://nexoniarz.github.io/Fry3DX/ +

Fry3DX

+

Frying 3D Extreme — a 3D modeller that does what you'd expect.
+Pick a tool, click, drag. On Windows, Linux and your phone.

+

+ Website · + Download · + Build it · + made by Nexoniarz +

![Fry3DX](docs/img/model.webp) @@ -12,100 +19,122 @@ guess, modes inside modes. Fry3DX is built the way people think — a toolbox li words on every button, tooltips everywhere — and keeps the useful parts (rounding, mirroring, combining, holes for screws, checks for 3D printing) one click away. -## Build and run +And it works on a phone. Not a viewer: the whole modeller, with big buttons and fingers. -Linux, a Vulkan graphics card, CMake, Ninja, a C++20 compiler, glslc (shaderc) and SDL3. -Everything else is downloaded while building. On NixOS `shell.nix` has it all: +## Download - git clone https://github.com/Nexoniarz/Fry3DX.git && cd Fry3DX - nix-shell --run 'cmake --preset release && cmake --build build/release' - build/release/bin/fry3dx [file.fry] - nix-shell --run 'cmake --build build/debug --target check' # tests +From [Releases](https://github.com/Nexoniarz/Fry3DX/releases): -## What's in it (v0.4) +| | | +|---|---| +| **Windows** (10 and 11, x64) | `Fry3DX-…-windows-x64.zip`: unpack, run `fry3dx.exe` | +| **Linux** (any distro from 2022 on, x64) | `Fry3DX-…-linux-x64.AppImage`: make it executable, run it. Or the `.tar.gz` | +| **Android** (8.0 and newer, 64-bit, Vulkan) | `Fry3DX-…-android-arm64.apk`: open it on the phone to install | -Five tabs at the top (Ctrl+1 to 5): **Model**, **Paint**, **Layout**, **Render**, **Print**. +Needs a graphics card (or phone) with Vulkan; on a PC, OpenGL 4.6 works too. -- **Knife** (K, shape editing): click points along the cut (or drag a straight line; Ctrl for - 45° steps), Enter or double-click cuts. Only what you see, or **right through**; then just the - edges, pieces of one object, each piece its own object, or the smaller piece deleted — the - inside **filled** (solid) or **left open** (hollow). -- **Hole** (O): press on a face, drag out the size (or type it in mm): round (any number of - sides, screw sizes M2-M8 ready), rectangle (width and height), triangle, or hexagon (nut sizes - M2-M8 ready, with a little room added for prints), turned as you like. All the way through, a - pocket of a set depth, or **only the outline**: cut into the face as a new face, picked, ready - to Extrude, Bevel or paint. Round edges then rounds its rim too. -- **Paint tab**: paint straight on the model (brush, eraser, fill, pick colour) or on its flat - picture beside it; the surface is laid out flat by itself. **Paste a picture** (Ctrl+V, copied - in GIMP or anywhere) or add one from a file, then drag its four corners to stretch and bend it - into place — you see it on the model as you go — and Enter places it. Pictures save beside the - .fry and go with the glTF export. -- **Pictures as layers**: a pasted picture goes onto the model as a **sticker** — drag it round - the model and over a corner or edge it bends onto the next face like a real one; its corners - size it (Shift stretches), the round handle turns it. Or put it on the flat picture and drag its - corners. **Enter locks** it as a layer (listed in Properties: hide, reorder, delete, paint in for - good); **clicking it in the Picture panel unlocks it** to move it again. -- **Files**: `.fry` is Fry3DX's own file — one file with the pictures and layers inside, packed - small (pictures as colour planes of predicted differences, then zstd). Old text `.fry` files - still open. **Autosave** (File > Autosave, every 2 minutes by default) keeps a highly packed - copy in the background; after a crash it offers it back. Closing, New or Open with changes asks: - **Save / Don't save / Cancel**. -- **Print tab** (Ctrl+5): your printer's plate drawn in the view (Prusa, Bambu, Creality, - Elegoo, Anycubic, or your own size), the size in millimetres (type one in: it keeps its shape), - fits or not, checks (holes, faces inside out, edges with more than two faces, faces with no - size) with buttons that fix them, overhangs shown red, weight and filament from the material - and infill, Measure (M), Lay flat on a face (L), Put on the bed (B), save 3MF / STL. -- **Object menu**: **add objects together, take one out of another, or keep only where they - overlap** (the last picked is kept; Ctrl+Shift+ / Ctrl+Shift-), always closed and printable; - **make copies** in a row or round in a circle (as objects or joined into one); **make it - thick** (a surface gets a back, a closed shape goes hollow); join flat faces side by side. -- **Layers panel**: **folders** (+ Folder, or Ctrl+G groups the picked ones; folders inside - folders; drag things onto them; a folder's eye and lock work on all in it; click it to pick all - of it), a **go-to button** at the end of every row bringing the camera to it, right-click for - rename, duplicate, show only this, move to a folder, delete. -- **Export** (Ctrl+E): GLB, glTF, **FBX** (Blender, Unity, Unreal, Maya; pictures inside), OBJ (+MTL), 3MF, STL (millimetres, for printing), PLY; painted - pictures and their layers go with it. - the sun (round, up, strength, colour), sky or a plain background, ground, - brightness, soft shadows, shade in corners, glow, colour; Save a picture (F12) saves just the - view, without panels or gizmos. +## What it does +Five tabs along the top: **Model**, **Paint**, **Layout**, **Render**, **Print** (Ctrl+1 to 5). -- **Toolbox like GIMP's**: picture buttons (hover for name and key), and *Tool options* under it. -- **Solid view** for modelling (plain grey room, studio light) and **Rendered** (real sky, - sun and shadows) — Z switches; Shift+Z wireframe; Alt+Z X-ray. -- **Shapes with settings you can change after adding** (+ in the toolbox, or Add): cube, rounded - cube, plane, circle, cylinder, tube, cone, pyramid, sphere, capsule, torus, wedge, stairs, arch; - **starter models**: table, chair, shelf, barrel, bottle, pillar, door frame, crate. -- **Draw shape** (D): click an outline on the ground, a side view's plane, or **straight on any - face of another object** → a new solid standing on it, added to it (raised), or cut into it. -- Move / Rotate / Scale (W/E/R): gizmos turn with the object (or the world's way, or — editing a - shape — out of the faces). Scale has a handle on each side; by default the other side stays put - (or the middle, the bottom, the origin); Shift scales both sides at once. Ctrl snaps. -- **Round, smooth, mirror** on any object, changeable any time: round every sharp edge, smooth it - out like clay (keeping sharp edges if you want), mirror across X/Y/Z. Bake them when you like. -- **Shape editing** (Tab or double-click): points / edges / faces (1/2/3); extrude (X), inset (I), - round edges (B), loop cut (C), cut in four, smooth, merge (M), make face, flip, separate (P). -- **Selecting**: click, box, brush or lasso; Shift+click (or Alt+click, double-click) a whole loop, - Shift+Ctrl+click an edge for the ring across it, Ctrl+click adds or takes one; more/less, - joined (L), invert, all/none. -- Join objects (Ctrl+J), reference pictures to trace over, layers (show, lock, rename). -- Undo / redo, `.fry` files, **export to glTF** (Ctrl+E), save a picture of the view (F12). -- F1 lists every key. If it ever crashes, its own crash screen offers to open it again, with the - last autosave offered back. +**Making shapes** +- **Shapes that stay editable**: cube, rounded cube, plane, circle, cylinder, tube, cone, pyramid, + sphere, capsule, torus, wedge, stairs, arch, and starter models (table, chair, shelf, barrel, + bottle, pillar, door frame, crate). Their sizes stay changeable in Properties. +- **Draw shape** (D): click an outline on the ground, on a side view's plane, or straight on any + face of another object: a new part standing on it, added to it, or cut into it. +- **Move, turn, scale** (W, E, R): the arrows turn with the object. Scale keeps the other side + where it is; Shift scales both sides at once. Ctrl snaps. +- **Round, smooth, mirror** any object, changeable any time, baked in when you like. + +**Editing the shape** (Tab, or double-click) +- Points, edges, faces (1, 2, 3). Extrude (X), inset (I), round edges (B), loop cut (C), knife (K), + cut in four, smooth, merge (M), make a face, flip, separate (P). +- **Hole** (O): round (M2–M8 screw sizes ready), rectangle, triangle or hexagon (M2–M8 nut + sizes, with room for prints); all the way through, a pocket, or only its outline as a new face. +- **Selecting**: click, box, brush or lasso. Shift+click a whole loop, Shift+Ctrl+click an + edge's ring, Ctrl+click adds or takes one. + +**Whole objects** (the Object menu) +- **Add together, take one out of another, keep only where they overlap**: always closed, so + always printable (Ctrl+Shift+ and Ctrl+Shift−). +- **Copies** in a row or round a circle; **make it thick** (a surface gets a back, a solid goes + hollow); join objects; join flat faces. +- **Layers** with folders inside folders, drag and drop, hide and lock whole groups, and a button + on every row that brings the camera to it. + +**Painting** +- Paint on the model or on its flat picture. **Paste any picture** (Ctrl+V): it goes on like a + sticker that bends over edges and corners; Enter locks it as a layer, clicking it in the + Picture panel unlocks it again. +- **Layout tab**: move, turn and size the flat frames of the surface yourself; the paint follows. + +**Seeing it** +- **Solid** view for modelling, **Rendered** with a real sky, sun and soft shadows (Z). Wireframe + (Shift+Z), X-ray (Alt+Z). The Render tab: the sun, sky, ground, brightness, shadows, glow; + F12 saves a picture of just the view. + +**3D printing** (the Print tab) +- Your printer's plate in the view (Prusa, Bambu, Creality, Elegoo, Anycubic, or your own), + sizes in millimetres, checks for holes and flipped faces with buttons that fix them, overhangs + in red, weight and filament, measuring, lay flat on a face, save 3MF or STL. + +**Files** +- `.fry`: Fry3DX's own file, everything in one, packed small. **Autosave** in the background, + offered back after a crash; Save / Don't save / Cancel when closing. +- **Export**: GLB, glTF, FBX (Blender, Unity, Unreal, Maya), OBJ, 3MF, STL, PLY. + +**On a phone or tablet**: everything bigger, the side panels when you want them, a bar of big +buttons (undo, redo, Shift and Ctrl that stay down, edit the shape, delete, frame). One finger +turns the view (or drags the arrows), two fingers move it and pinch to zoom, holding a finger +still is a right-click. Files open and save through the phone's own file picker. + +F1 in the app lists every key. + +## Build it yourself + +Everything it uses is downloaded while building; you need CMake 3.25+, Ninja, a C++20 compiler +(GCC 13+, Clang 17+ or clang-cl) and glslc (from shaderc or the Vulkan SDK). On NixOS, +`shell.nix` has it all. + +```sh +git clone https://github.com/Nexoniarz/Fry3DX.git && cd Fry3DX +cmake --preset release && cmake --build build/release # (NixOS: inside nix-shell) +build/release/bin/fry3dx [file.fry] +cmake --build build/debug --target check # the tests (after --preset debug) +``` + +| Preset | What | +|---|---| +| `debug`, `release` | for working on it | +| `portable` | a Linux build to hand out: SDL and the C++ library inside the program | +| `windows` | Windows, cross-built from Linux with clang-cl ([how](cmake/toolchains/windows-msvc.cmake)) | +| `android` | the Android library; `android/build_apk.py` makes the APK ([how](android/build_apk.py)) | + +`tools/package.py` packs builds into the downloads (zip, tar.gz, AppImage). Pushing a tag +`v*` makes GitHub build all of them and put them in a release (`.github/workflows/build.yml`). + +## What's where + +| | | +|---|---| +| `src/app.cpp` | the app: panels, tools, the view, touch | +| `src/mesh.*`, `tools.cpp`, `cut.cpp` | polygon meshes and the edits on them (tested without a window) | +| `src/boolean.cpp`, `modify.cpp` | combining shapes, thickness, joining flat faces | +| `src/shapes.*` | the shape library and starter models | +| `src/scene.*`, `package.cpp` | objects, folders, the `.fry` file | +| `src/export.cpp`, `print.cpp` | GLB/glTF/FBX/OBJ/3MF/STL/PLY, checks for printing | +| `src/uv.cpp`, `sticker.cpp`, `image.cpp` | flat layouts, stickers, pictures | +| `engine/` | window, Vulkan/OpenGL renderer, UI, files | +| `android/`, `tools/`, `.github/` | builds and packages | +| `docs/` | the website (GitHub Pages); its words are all in `docs/content.js` | +| `tests/` | `mesh_test` | ## Coming -Sculpting, materials (metal/rough shown in the view), scripting; animation later. +Sculpting, materials shown in the view, scripting; animation later. -## Layout +## Licence -- `engine/` — Fry3DX's engine: window, Vulkan/OpenGL renderer, UI, files. -- `src/mesh.*`, `src/tools.cpp` — polygon meshes and the edits on them (no window needed; tested). -- `src/shapes.*` — the shape library and starter models. -- `src/icons.*` — the toolbox pictures. -- `branding/` — the logo (SVG, and PNGs made from it with Inkscape). -- `src/scene.*` — objects, folders, `.fry` text; `src/package.cpp` the packed `.fry`. -- `src/export.cpp` — GLB/glTF/FBX/OBJ/3MF/STL/PLY; `src/print.cpp` — print checks. -- `src/boolean.cpp`, `src/modify.cpp` — combining shapes, thickness, joining flat faces. -- `docs/` — the website (GitHub Pages). Its text is all in `docs/content.js`. -- `src/app.cpp` — the app. +[Apache License 2.0](LICENSE). Copyright 2026 Nexoniarz. +Sky pictures: Poly Haven, CC0. DejaVu fonts: their own free licence (`engine/fonts/LICENSE` in builds). diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 0000000..8d6b5b5 --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/android/build_apk.py b/android/build_apk.py new file mode 100644 index 0000000..1abbfc8 --- /dev/null +++ b/android/build_apk.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Builds Fry3DX's Android APK (arm64, Vulkan) with no Gradle: the NDK build (cmake --preset +android), the content as plain files in assets/, SDL's Java layer, the icon, then aapt2, d8, +zipalign and apksigner. + + nix-shell android/shell.nix --run "python3 android/build_apk.py [--install]" + +Out: dist/Fry3DX--android-arm64.apk. --install puts it on a phone over adb (USB +debugging on) and starts it. The first start copies the content out (a moment). +Signed with ~/.local/share/Nexoniarz/Fry3DX-signing/release.keystore (made the first time), +or the key in $FRY_KEYSTORE / $FRY_KEYSTORE_PASS. Keep it safe and keep using it: Android only +updates an app signed with the key it was installed with. +""" +import os +import secrets +import shutil +import subprocess +import sys +import time +import zipfile + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +BUILD = os.path.join(ROOT, "build", "android") +APP = "Fry3DX" +PACKAGE = "io.github.nexoniarz.fry3dx" + + +def run(*cmd, **kw): + subprocess.run(list(cmd), check=True, **kw) + + +def app_version(): + with open(os.path.join(ROOT, "CMakeLists.txt")) as f: + for line in f: + if line.startswith("project(Fry3DX VERSION "): + return line.split()[2] + return "0" + + +def main(): + sdk, ndk = os.environ["ANDROID_HOME"], os.environ["ANDROID_NDK_ROOT"] + tools = os.path.join(sdk, "build-tools", "35.0.0") + android_jar = os.path.join(sdk, "platforms", "android-35", "android.jar") + llvm = os.path.join(ndk, "toolchains", "llvm", "prebuilt", "linux-x86_64", "bin") + + if not os.path.isdir(BUILD): + glslc = os.environ.get("GLSLC") or os.path.join(ndk, "shader-tools", "linux-x86_64", "glslc") # (the NDK's own, if none's given) + run("cmake", "--preset", "android", *([f"-DGLSLC={glslc}"] if os.path.exists(glslc) else []), cwd=ROOT) + run("cmake", "--build", "--preset", "android", cwd=ROOT) + + stage = os.path.join(BUILD, "apk") + shutil.rmtree(stage, ignore_errors=True) + assets, libs = os.path.join(stage, "assets"), os.path.join(stage, "lib", "arm64-v8a") + os.makedirs(libs) + bin_dir = os.path.join(BUILD, "bin") + sdl = os.path.join(BUILD, "_deps", "sdl3-build", "libSDL3.so") + for src in (os.path.join(bin_dir, "libmain.so"), sdl): + run(os.path.join(llvm, "llvm-strip"), "--strip-debug", "-o", os.path.join(libs, os.path.basename(src)), src) + + # Content: the folders next to the program on a desktop, and the list it unpacks from (a + # version on its first line, so an update unpacks again). + for d in ("engine", APP): + shutil.copytree(os.path.join(bin_dir, d), os.path.join(assets, d)) + commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=ROOT, capture_output=True, text=True).stdout.strip() or "local" + version = f"{app_version()}-{commit}" + files = [os.path.relpath(os.path.join(r, n), assets).replace(os.sep, "/") for r, _, ns in os.walk(assets) for n in ns] + with open(os.path.join(assets, "content.txt"), "w") as f: + f.write(f"{version}-{int(time.time())}\n" + "\n".join(sorted(files)) + "\n") + + # Java: SDL's activity and helpers, and ours on top; then dex. + java_src = [os.path.join(ROOT, "android", "java"), os.path.join(BUILD, "_deps", "sdl3-src", "android-project", "app", "src", "main", "java")] + sources = [os.path.join(r, n) for d in java_src for r, _, ns in os.walk(d) for n in ns if n.endswith(".java")] + classes = os.path.join(BUILD, "java-classes") + shutil.rmtree(classes, ignore_errors=True) + os.makedirs(classes) + print(f"Compiling {len(sources)} Java files") + run("javac", "-nowarn", "-source", "8", "-target", "8", "-bootclasspath", android_jar, "-d", classes, *sources, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + class_files = [os.path.join(r, n) for r, _, ns in os.walk(classes) for n in ns if n.endswith(".class")] + run(os.path.join(tools, "d8"), "--release", "--min-api", "28", "--lib", android_jar, "--output", stage, *class_files) + + # Resources (the icon), the manifest and the assets; then dex and libraries; aligned, signed. + res_zip = os.path.join(BUILD, "res.zip") + run(os.path.join(tools, "aapt2"), "compile", "--dir", os.path.join(ROOT, "android", "res"), "-o", res_zip) + unsigned = os.path.join(BUILD, "unsigned.apk") + vcode = str(int(time.time()) // 60 % 2000000000) + run(os.path.join(tools, "aapt2"), "link", "-o", unsigned, "-I", android_jar, "--manifest", os.path.join(ROOT, "android", "AndroidManifest.xml"), + "-A", assets, "-R", res_zip, "--auto-add-overlay", "--min-sdk-version", "28", "--target-sdk-version", "35", + "--version-code", vcode, "--version-name", version) + with zipfile.ZipFile(unsigned, "a") as z: + z.write(os.path.join(stage, "classes.dex"), "classes.dex") + for n in os.listdir(libs): + z.write(os.path.join(libs, n), f"lib/arm64-v8a/{n}") + aligned = os.path.join(BUILD, "aligned.apk") + run(os.path.join(tools, "zipalign"), "-p", "-f", "4", unsigned, aligned) + keystore = os.environ.get("FRY_KEYSTORE") or os.path.expanduser("~/.local/share/Nexoniarz/Fry3DX-signing/release.keystore") + pass_file = keystore + ".pass" # (its password, beside it, when made here) + password = os.environ.get("FRY_KEYSTORE_PASS") or (open(pass_file).read().strip() if os.path.exists(pass_file) else "") + if not os.path.exists(keystore): + os.makedirs(os.path.dirname(keystore), exist_ok=True) + password = password or secrets.token_urlsafe(24) + with open(os.open(pass_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w") as f: + f.write(password + "\n") + run("keytool", "-genkeypair", "-keystore", keystore, "-storepass", password, "-keypass", password, "-alias", "fry3dx", + "-keyalg", "RSA", "-keysize", "2048", "-validity", "10000", "-dname", "CN=Fry3DX", stdout=subprocess.DEVNULL) + os.makedirs(os.path.join(ROOT, "dist"), exist_ok=True) + out = os.path.join(ROOT, "dist", f"{APP}-{app_version()}-android-arm64.apk") + run(os.path.join(tools, "apksigner"), "sign", "--ks", keystore, "--ks-pass", f"pass:{password}", "--out", out, aligned) + print(f"{os.path.relpath(out, ROOT)}: {os.path.getsize(out) // (1024 * 1024)} MB") + if "--install" in sys.argv: + run("adb", "install", "-r", out) + run("adb", "shell", "am", "start", "-n", f"{PACKAGE}/.FryActivity") + + +if __name__ == "__main__": + main() diff --git a/android/java/io/github/nexoniarz/fry3dx/FryActivity.java b/android/java/io/github/nexoniarz/fry3dx/FryActivity.java new file mode 100644 index 0000000..f1b4fc4 --- /dev/null +++ b/android/java/io/github/nexoniarz/fry3dx/FryActivity.java @@ -0,0 +1,11 @@ +package io.github.nexoniarz.fry3dx; + +import org.libsdl.app.SDLActivity; + +// Fry3DX's activity: SDL's, loading SDL and the app (libmain.so, whose SDL_main it runs). +public class FryActivity extends SDLActivity { + @Override + protected String[] getLibraries() { + return new String[] {"SDL3", "main"}; + } +} diff --git a/android/res/mipmap-hdpi/ic_launcher.png b/android/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..33866dccdf2a6726e422b3e2ae0650af1fb39959 GIT binary patch literal 5955 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 literal 0 HcmV?d00001 diff --git a/android/res/mipmap-mdpi/ic_launcher.png b/android/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd20442c16e3c7b85afb0ce7c87edd3ab56e103 GIT binary patch literal 3642 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 literal 0 HcmV?d00001 diff --git a/android/shell.nix b/android/shell.nix new file mode 100644 index 0000000..ed084ed --- /dev/null +++ b/android/shell.nix @@ -0,0 +1,20 @@ +# Android build environment for Fry3DX: SDK (platform 35, build-tools), NDK, adb, a JDK. +# nix-shell android/shell.nix --run "python3 android/build_apk.py" +{ pkgs ? import { config = { android_sdk.accept_license = true; allowUnfree = true; }; } }: +let + android = pkgs.androidenv.composeAndroidPackages { + platformVersions = [ "35" ]; + buildToolsVersions = [ "35.0.0" ]; + includeNDK = true; + ndkVersions = [ "27.2.12479018" ]; + includeEmulator = false; + includeSystemImages = false; + }; + sdk = android.androidsdk; +in pkgs.mkShell { + packages = [ sdk pkgs.jdk17 pkgs.cmake pkgs.ninja pkgs.python3 pkgs.glslang pkgs.shaderc pkgs.zip pkgs.unzip ]; + ANDROID_HOME = "${sdk}/libexec/android-sdk"; + ANDROID_SDK_ROOT = "${sdk}/libexec/android-sdk"; + ANDROID_NDK_ROOT = "${sdk}/libexec/android-sdk/ndk/27.2.12479018"; + JAVA_HOME = "${pkgs.jdk17.home}"; +} diff --git a/branding/fry3dx.ico b/branding/fry3dx.ico new file mode 100644 index 0000000000000000000000000000000000000000..09318c38c562b9e4faf00721680b4defb0401731 GIT binary patch literal 37956 zcmagFV|XRO);7FjW5>qM#I|kQwryi#PHfwDCbm7XF`3xOMDLt){(jf@)Q|33Ro!(} zt?E^^?t4`O0AK(Z04gfrUm^i8LjV9C|8h7u|C4`00RZLy%1B85Cs)A&0A_Fi029;y zWFBMy02vJc5EA;Itcwl+NRa>lK|%kMX=wp~V%GoU|C2}n9_#?XtN{QJrKBK<1dj** zuMkOEN=)V7{XacGg@yiiCBFYM1^~ccq{W2QymI7|<*oG0aU|xRw;4UJjw~tU7b#k! zaxUW3Y@+1U$U zIVYx>Sf2x~-@2q|^DbSzJjR^!Z*RwDedkK5yjcl?o(m=exgxx;)m8%X^1qJJ0i68& zgsZD^YmBN#CSRVi}>;dC5UG78!|0CWt5+ncg1JOUb5sou3fR?bi*Ow#pM| z@XYW)XJqwO)dB8Ssu!6PbF+e&+)Kip0QanB#r)u9*SiJf_{V9N+o4SYLhs0;$oH-u zNzXJd)>gm|N#hEgGWvMeA-G~%&d$#P@XKDHNAE|LX{)(}Z`bcLxz6S-5-%qzBQJG` z*dElhc0Ablp%3!1W?wsh=P>lv?uRs0#0R`v->B^Vb$0C?( zf-knP7QBq#aeS=V%^MGDsyE1`1~`&UA)I6Qh^5?7Sj+G)#lZ2H$P@Z9WDLC=w7L&W z?Mhlva|m^Cs;c;O#}+U9qP^NN**&yXTN$?Equ9c#@>Ptw5ABO0 z`em;}iR|k?aQ+c0y}Q%iS5XcEec7zeMT5TglijZNO~a_{YU(pi19B(JX!!+6?jX}z zf=?_gPRO*ZX-D|dQXZ6UDb1)420KA^^-NbGE-$CwDpE-Z^kI@Lzb}J!@tn#II$CY{ z)ZO2;IBmeAHc@267#!_tW;d_+Y=SNckj)!*LUu4z5-N*~1nnK_ z{NZmHV<6&gSfppu|Cs_nT3kV_PQ)nW|HEMZW1=|!HJCk5{uclMD)fH_6PKoIN2CcH z3v_+D@O<2MCMYk9FCHZ&=d!LPDTQrCRfyeG?^zA(Wcxvfj(h&7Em)GI=r;3;{DHc0mpM93qyF_;OeV zHZ89H{9QJECle40xY!kZk8C~hRwoYy6cBr|OQ7~geDVGrSC3_mgq;mLOlMTgO$!bV zR;gV6+s15R2)pxrjh7!p771&6jwGVAp(6wEB5qU87PC=Mvyd|rh1E|8O@|dr4~^e>2|V! z33M{Z&cttGJ8bs#ys1q_aXN?@LX~{KY?bBX{iJb}n==N*gbO+9CZ;~dPTMBC0Z1Xc zVxJ8-2RK#c7JR!py1NHAbmGuai1rfZ{Iby#{E1j1>d6V-CXGk){j(h7{gKmaR2BJQ z-Znb|3%>g#kV zzQDhoFt~K+dwIj@!~mP8@Nt4sF@6?Hwt>GCxT%}|-H^=*u+1ykv9+Rc#7)0w&~|Y}bz6==tP+hR+#Z8s)H4TRNAxXo>@}$1MU8q` zJf6_ba@de8>z%T=JJiND)n|Oud$;4+VpsWwSye zW{&2&M1c~`;-XIJ38{PVS`eG=L+e>K8%qRGOD1KSHUu2GSvs)_4PUFql8r_358u>d zYzN4jq@0U5GALfF(ozNufK&lnjZ~UOWB7sb?mvB#Ocrj7;JGZMF~-j zMyWzXY;&{hZ_YC%vJ!$4jP0OfpjB1moLJn=lD5ivRW8F%-PW`5X?^md(ouPZYT{4~8B0|oAgA`gouzCD4 z0+Ba0HJPw`lamQo1zJRVQTNP6&^(imZOZnBL?09UiCb257&IRUDRTPZ?V5 z_1QYnDx>A2D6P}zW11Hf5u9W9wn=JPQz`Z=7vxc6A_(6j-xHP8*MAwC7O{rd9Jv0( z!;kgyGaOEbMktXcf$YeoshZb7jSV&-KX}Ycxqaq4)Fg1PZ_z5GHm3ohg%Gn>jh}qO zj#G(DF*KC({@Y_|>c|O#nyd_4t0pgBiH_uk7sRwFK7S|H@sk}pIbv#frLy_~)yOhJBPVcd@)MOh26{cW8V0ol_kmM`sC=3{ z3%&ox;!X0ep5(uax2uoO0{{T){J+J!HeJ6)PaShC(DvHZ;$$|L7yIgx-oDe`dMT|9 zmSzPWixgClAq82S2oojt$^>Lr1}(r4`!O-&p^Lu5+V};{-&ns$p$|Y}R)CbK4oc_K zR2b~7%b!iJ{PJ9%bOdrTH?4yy*Cu>2_|Gp|p8{(icWvP)(;!;DOt-sgS40Ydw_r8j zD$RVK1yln$br&~L`!(tf^sCeqy4PcEgV-%F`a_>vhsC7Cya>IF0zO}^%gf6*{j(RJ zE$&Zds!0OoeFJ-|Of0VFt|}cO_j1TW8>Z+weQa$0AoauTwb`w!*l;B0?!4VM(R~An zxx^>$vGW%{%83!jGFp`uEHto2*z3;KapkN52Oh2y|8ugJkw~ z+y-+7X-F;6-R|W7vT}0(EI&H<)K*nRdb4^lIx}eIXF4w0v}WWw&R>;??a&)ID+%uM zYKu)*SAiQsiLKV^YGP6=a8uIKq4dln#6KZBgGiX#=P5j#j>Roqm(m#z0^gADvo96z zHXabkhD1}MnMserz6nvP!YA^wr%`~4Y$9@|I3`Eq@W(`*R6#7p78D^B!kkDcCJC-0 zX;59;4Ee9aJ`SBvXl;IG_E-8xRwYm! z&sz_VQ(jLyn;lm%*i7B?!@Py-6EDl_hjyDx62-|UX97krh^IN+fE9G+=pHAtx}jIg z+|i~5P*t$*EN0&8n*!1HEr?V|kDd%i`66+F#gQOq|EW9A=zVX>j&R1xTmp8|jHyaBYi!@OTPT7Cy;_AFO^T>Sxt7$w=g%Dl>tuhFg*r(Dhic`kT_AIQg#CzS_&@ z+S%rjp_tsb*EGl9BJ-S)O0}Bsc%Sgu>D-WVVJKp4i)-9nHbzR1l$l)hx5d8()$wv; z$?ge9X`w=pwTN4^>z{!*sIeiQkvPy^Fjy%qxLPbKCx2tcxLiFLH=9KDnz}?JL3mLI z<9+la@%$0z%UE4o-rC+U+Dauz zLqnok)x5ba<-K0?o5r`5AckVy*FMi&Icij^)G{0-e!A)i0;`%Yl=TtMi$st#YN?Haxsy!4uH7nKOgH&Vi zr@-DWQ2up$YQSTTpsuUla@Mczi+HJbGRr)AIUERUoI3pT`%Z19Hal&RM#rUk@ z+n@R0BJ$z5(PYs@W*<77rUJ~f+%s#=-bQ0BcT?Elj0BqS$kiA*kSnIbOL29?Aq5f( zxVyZ+Shq3oqzitxq!Y^|K`pYG*FXv|I8)&l;%ef?5gM>2(G>_Qt3#F^rD&d}_&}pv zAn<2J*3lv2c+bM#IqVlX92G%cb~X~D%B zf-B90XC084?@ypWjL9=lG1|FrM6zfpqQF#mwC23or+W3()9|r~<=Kbld<{02*Ts~6 zf^4!{ea>gC?mA?lbLkrTNE_Onr|5k#)9;j`IbA77m7JVxYH@`L%tURF zuxT=0zD=%W)~s{iBU~CY)|6lSg|TCY^9ZQ|LE3k=h*ndHgJ115;37OJVz2S~dQTnb zLtgw_>nLX6R7>pso7xN^6YG6Q3k#X*%kR)rS6aef4h#)8TgPk~sN+Fm_o}iCl+aKI zkr8EGeD6M^yJZlPo1|5PKIgami80pw|5PZi;jDpAh3vR~2s# z^_^aG<*(Ktd{)m0VyE<;rLyOCHcy~5Nw*t9cdsG@ds60+Mzw$~U(h!S%IeO!3f$@| zZa0m;1HYB&_0XIwIuFdbt+N5|Up`YhF!pmyi}3F2izl<~*VLScSqXcc6%rFcUd$}~ z$BFo;>hj&&iQlp^*Sj0+)}y~$(49Yq_9m!v;-E9XtTPxRb8wY19dJ>q3=Uw4B6Fw_ zW+>Nq!reEBv}!6-4hVF#Xu`lG5brKI1yxRZ0CO_KM&d^b_KdFWBwRY;>OEht2RNiH zdLAV3`{v30t7n2cVenc62gj2U6u zv(EebLlL|&#p`#+Q<`BUC_I#>&(ecFB@6IBd7edtNtmTVNWsnX1#A3si?9z2WEsmJ z4nZMRS1z-40xn162UgG!L)P87TaZuWtgAu-IZxTpWP)oGQJwO>fe(s*&U2U;pg;E2 zh^R2$dBc}Jd93+}@$swErj9ENB5WKToq7e_ueaR*Rq9r)I_uNZ)7|l>UogIQjCk_V zN%a4Rr}7`Bhv~nbiYEAL#6P+R?0=rhOqO@7{)+#YV4v&trsa-Z-pHsfA99;>+<*~! z>`ezMQyjcaF^_!>7Q4+iF)P{ZD)}W1_3DzS3Yu2xFePPnA<;tiQfQI$1`Glcp?(ez zJ8!%1_I7+kC{JEH)7kCYzO(g#ThAvyZ|OLcW$u&XcU;nTpWE|Yufoox|Ni~^j{8@@ zqsk=l>~GoI5(HiUpOtFeTvko(bB)OPs;VW1yCn*svbscU;`h9A$IJC*%OgKKbW{95 zyPcl(%`W$9{idV4IyISIa62F~KZqFNin^q#qDy#wOl*t)?dg(Ne@F{|DafuFM)$JG zoR$;sQ1HOpBG?sK-M#A1rywNiKqQW7>;Sjt;Kt38_sPAllZU(Sj# zT5SdI(0jI1c;%%^G{Pefipk&KpJ)*(?dEo*APapJfDAEYHn^;dFN>uiF zfc4ut=VAdYH={VQ%&{jiYLn!#KsCONW_c2$+MhQd?152k_q@|qd{qM&zH10A+9`kH zxTZM2Z(y-NK2sdpl<`x98okAd?Y_^L>D=UF-GtuhDaXEu(=n?Vi%72-JK!Ab>RIdu zU?rAnbpsO}9H&@Y$vXBEfu0WZk|)`#Wnx#;e>dXVaX6d(+NxUz+DQQ~uj%`%Vi52F zcot~;uIWKYj+L;mm;lT88i!cOKPiirjP;2jM zdr45G&>TsOY4>^|2Y!y=zrI_pCfUpg^+poXqpk=TAG)VE-l|S{U#)y>J-;9uZ2QN0 z@hQl<;%TZ!tCA<*ygp1S7_5Em*G%ZJ+pJ1-htSYSVzj%%iIKF(_)^T;+n(Gl&w}xh z;Q(C@#eEp9k7fIO5!xL(a>|}8s8Slq4squ8yxkdFUO^I}*_<^8WwvR}hnJ#!4Ug&p z4X6HDD$-)0*)W%J$47dKmdWPp98h+=JSCis1P!bbQi?+Oni89-zB?|K78p)i6=E6M{0gsl zjkovLoKQ^!W@*pb4*gs)mCmM0W*S}LG@9@Ea7nMb<$J%*=ww>9!1^?CJ>KD`@0I1$ zHUD^wBjLbQ8FMD&EO$=KiSGH2j)Y%n`}38+fUj&tr#I|gh3jBKJMAN8tdK;j8UXF7 zB{#EPP1kOiji{JFkgL)3GERen^h@{(g05GJw=}S1eHLiQ&1L*K@OKT?&@hW6TaH!Fh(O(!f5&K>_xk`?VGSIjeOk&4nt>DXELtvG|psU4E$bfL79pu>iM#U z{v>CJ)W?@Jg4D9!0$K_|_Nu${uMG-~SMRI$PDGm?xV@c}cP)3a;On2P{q~x(WSFd6 z=VfEXs?5_qc4}dk9w<)dsjlm7_eY^ZOEyJ1(DtMyI3Zp@ry)8S9PsqWRx4RJqT5z!I2z4xDgLx()*U~u&ZZ>>*S?IpYhbMClL_R-s ziw24uyXLR+@6tG1k{Du9Eg4M8)z-;0`8pp;>T-`?`$=d#=CbuS4-`XBhBsQP;AE** zGcjE}%v8aHR^m~(TyAC43CO3~?NmtcZ5I}g+T>_##s%ES*C(l-E+UIm#h4?UB8xGj zI8g$UG(YZ)T7rcstn3htm05_K_o5YE`(F`Zkj}npkdd9`c zh4)z=+}?C%a1h6T~Dt(#SOu^FueQfN@y7l=)imG{Et$N z&kwPrer_}qpt49<-3eqH^}tgw_?$iAahl@M>ktyX*@4Jnzf}RBo1Rwy*F?p{d|K3! zIY|r0VA-?E z{hb$U`f=}&^9i}AUvZ*`zCFU8Q0mpmw?f&Qhc_q>KA-pamjI}L6pjmu~#(btIl|}xIE-eiG99B)6#=~8D5S@ZZe@nSb)KSPfFv4 zm+)5c;%|+WYP9f3%u1UdE2Fvv>k*Du$XqF|@9P^}KQF^=Pj%}q;kY$Xkz?_mkn#Vz z-VAW%PzwvF$r!I#vV&(mksQ<7J9m7m)!P8oIAAl_et(@2LR2y~)9Z9}4C~v(M`K;e zN2L_Y@BvjzYvk`o7JW^Sk@7;*agNE~wn6S#2$lp6FJwhBKdI~U>% z3SJ<2RR|tUDh##ihcnhlGjr0f-Tn|GV8JCdtxBe8r6B{(SB(t0K=xetzE9PG82ydB zZO2`@3QA1p6Hy1lxBtdQOx~Xh@k5s>G_OEN`=Sv{(o%~4mE^yA(c=xs9$OfK`h18Y z#o~}$@icWz!e_9BR!2sG=KGY$i-!NO6bW!O#c!{(h5~+`kAU@hR*d)56q4bR!ZKns zCf6`=L%VflcS;Xo`l0)Gn%ak7m7pP~o?XV?7T$ZAc*3c7*r?N5DByvUn?qb}3yRbo z{g%SL%FPd500>gN50ta?ED8!JzY4;un+g?XNr=0({1Ka8&7tXq`Q`rh+X;$SX8;4j zK^Kk8IAvvd)OMf^(%fxuv`Tl^bXc8&rU^<5T!U<|tql3@(xAh%W#0u+xq%EFt zUAIzS6;PeMnO~v_p4#v?P+S6BE(JRi`+L&&w2XPRHYG6kg5ddN)DWRFY1k~3g`|*R ztT?{_r!64sNyuxj@Yg6erV|VWq*SSlV|+q+xOfS@3BP|ZdJ&!KG*m~&0Znu@?4c==2ax}h(<|yS;(jmhbwmZI)WjDfMj-kU#{$dtV-yEOmg-( zC_+3|Op*LtyRs3tNbhTK0j*4F;D|P30e&+<(<&P7I!@aopNez@HIUE9uVUKvJJ@>Y z&4$tVixO@ipHMVuw_=Ky<8l(yf`mOqUUD`MkgR`5eY%&lO4lSdV2ImSM$Au%H}_gw zy-DExavz>|haR04YOB3{{Au7v4RPmM`g005w-yBsLWi$r)H8yNEaDC$*VT?X2nPWN zhwlEE|JdfX&#JE-biiw=h+Di{!4sPMkl{X?lZTXN*^82-<}SQ4LID*9EWAib3C6VE zuVp4Th7@K79o05bA=mac`07pk4OyyiWK*k~hV4}1)(ECx{L>AfMvczEv0B&hyw7C) zE?8mg0C@`5R$S~`e3~g4%ZyO;J!X}YNRA-INoYZ2G+WP$ub?K8eX-1DC%iW#=8ufX zAOGebV9cXZkHYQU9~bLOj7v9@1F!e|{T`m4p4^tbpg}C)S*C+IsZRh?F0|!2D`>@b zDuzs*Q0saD^1q#y?k%0^XJNZ667}D{% zi8sTn4knK+LoNKaPXfV8E~Nsq4p9UD59BEHuU_cC$g#@Wc?SSM2mKE@u4H@vAY5^e z=yP@S?BjIwi2Ei>7K~+P{o}_E)LZZra;7!RX`9&ey&%GJ6*~&G1Eg}AHRn{*F}O0h z{>DQn0!CCsuOSX2MA*po23S-bx^GA^s8$*?f4kmxk_EeVr}1a3EJ7hE%K!Ya-szs= z?(@A`ci@yunC@~v2k%#C{>o3H>B%~CORJ=q67)ZgX|??o{_|&BSz8-lEtCN*9uC#Z zs08Q3I9b9sb;E?iz(5`nz-fM9@6X3oUBZ6mu^M~TN6X5;kNs!`xNLlge#8<in#)|a!yQpowvGovPw>_MVbjJ-0}5`%y7vDEejVE# z{QqpC2Q!@zSdJcQ{90|hOBP1mg4M|{v)0K}60;MKFesmijKoX?%(Ka>s@I|n-RRoe zkNLqnxOWB<6jtTdT)bBy)UtDOpDyaH{Yq{0g*pOrtVPPX$F^U;8}_VXF9Z()a~p}q zjLBui7cZd@JpI}YtN#%lS@q{f>*tz$HpeAN@-}`@b$u_MQ10RVYL?D*?x{MuLG^~M z#KF)1$fqxS1)bCDonY_(pmTb#032k`q=W+Gr&zpLFoxOjYOSIJRIGNpgMRPQC!ei- zMHTwrsUcyIh!K4isuu4-lL4)Yx@9fWKt4=^!!{3V5twW`L*M4c9@}XSI(HALBSnXC zMb7+oa{?9Wfts~LRe2d|BF|UWFjf_pZLK@QPv3t^-G#$}c=^xN_M2Su2US?m_*5V@ z5h|*VA^7r0ACfY7U&C#^u1s~je^BlxvCLhck;oGUw+0cUxuZ;@Bu!AoxMuzY&}lc7 z2!Go!jA#Sh{YHo^egb^?rI! z{>k3p&@@~mF=HtrFZs!ESR7!EAk~aA5P0n$VdJ;_bh-Hq*B=5k#(@cc%K%*B!URY_ z3PyE7uO^km&t4hGKuvj<*PSdr*PEU-y`oZf6ygAd)f-1(qe(a0fA%ABI_^b0^GSR; zO-`Rc50a3$$46xV3vzrWE6aOv96C zcOA*(RqU9uyxD61Il#SP;U&Pz=zTWJEC0^97`R! zMozqy2~e85yYppt+w&aX-^gnOKI6V^f0RI&*RbQTc1kmI3!N!tmUQQ<+r}T*8g^zR z7&yT*UgMK6pr|U#)XV5I@9oQ4E|7Ug*x-0S^JM zgG3Bb*(|cWlDt0c&2d7(pZQCS9cll9Z&?kU@ddq+t^t#|w+7C$=*w8J+2=1aj%_Ya z5%q_C0ZyEjt$1klO7=6ZDEB6FO!B2v?bI-rz)^4}4zb*hFsx5cNzR$lA7x1+-U&c+ zD)mnrcmBSP8>Ac@fw>V)|6EVQTcm*J7X~|3IMrwh$RbcjWmH2PIg$4t?VYbi{LY(e z2cxUHvChwoP}UQwMi?LpRY($-qJP}+C9TE%_^dLQ z8i)wMB&ED_k}xXBY4S6GAl7dGG52MLAnGhr$T9hk;F%bu_<)m#8WPM~m^7yx)_dFY z_&k`Lsp0>`srx(c6VH7j(zS;ARMVWx0~OaB0u+h z_cLPCnhJ{R+EtWmvVc9rXXKf@Wm!wW^NCMj1L!&vPM_oXs+!|=V8g)YO5eWKn%I5y z0YRAp-MPO8j_}9kcLIOz5D}S?d7yI>pgM;ut#x%@tut`S;7>G|!G~1-S>g$cq9}wI ziAP@AM&uzD$89IEJ%w{Lkn}NH0q_?Vl>x%UBlj5LI&Th>G5t0DE&pI^DqxP+qHL>y z#YFkTYk(51bTA!QXvsv{6n?T1`SWcOk^``E789r>!6fJxZGj0{?`0@$;C8^O3YEtm zQYN#JKEFZWG=uBveOLq1WME{ehvo_Ak5XtkG}VH}sL#uqzguqpd9k%QM>(sN7B9tHb9EMH4mqLKKRz(`e zY^cji7^ME+B7pDIFKSVU@8@4`_sj^kC)fkDbv52ACwq{ZVM$^AgT?$#Q}?LRi};p} zlGLXiI-4|>J%SS<)9uq(3LzZ~KF^zppVU^O@2b*xg zSZ%U9&$;K$cF*^w$w|}uIuq6JWA-qxErsMX0IPZhcnF2TGjDql-TS?DKRDS^I`*sz z>vI}fH=9c}rGL(&#^JREL!zMsa>#a=h3(gbLBsW@lgg987peteccnVoc`W0UmRkw} z%8G=H`h_p{E^8-sPqBWNrqM${hU1D4YhA6Mb2ni)5BiO1$Cw0sB^_!q#)b>n>aYyV z>6THzi*76FPtZV%L)4v&7mG3B&aHsEWmeb90e8U@WYboQuqI;Lj@@tcNJw--SJ z2T`$Pvg%<_&DqqHk6Vq^>G}47nXc)W9T)QQbh+rkG!7)$OV$)_3xlj5#k_gY>@94+ z{(aAXFx-*F*S8=PJ+$L1=P^4NYJ{^Ed~EdN<&1Nd5APJPuob ztglu7kWoR297wc!Pm&oy@kH$Bgbk+ept;Pj#%=wz3l8sBwp6{-5qBuyJN_oCgEehq z!Bu&9BKUNBXwT?upqog$voYSY0f1nGZ1BZ#2p|gB##L%#pSG(ZZAn}9O>%n(kWi<_ zLoM!MgZ*k*aM}rp)K!p&LlDAEB87#Bv?;Iz^mb3&*3RTvK2f`qwqUT#(-yM6K{yrU zUPtZw#~>NCt)x+%gd||GjY9A&;90eS2@1|Mx?#OPYs_I2%X*h- zQirWCR9RVKQk$t8o4OhM`ufgwxOIp~uyA_rtH`R+_AFDId5-e9FfvW9d5uJu1m(&NAV98J-fOTYBnnT)OebW6+1{s0{qyW{(7|Nc8=b)eQU{HX+yFXE?J- zz!*tns(!N|Ib@Aelq~g-S~L76PbKU4lHh@?gO_gaK_d=Fsh0RR9idGs1Ow2r1ZRS}0EQX&@RA#}4K>+WH zN1w|cEG2@p3NzqQZqlAt1+Q2poXZ~x=N4SL$g~Gw7S435gt(V|j*!!|b`WtF$)M)( zj2rM7a;(XylXnI5Ik0mhk+JHJK1oQ4Yu)Lj#y5TFv6wCw=!TKfOKW78n7p&t&%NKf zfEw(PAW3+&k8}D`7>l^3WzDfLe!##zi^PT%)n41#*p&t)2U>uPwVIhFQC$s%c3Kp!aCP% zbxt{Nqr(NRom<74h4xu1VFE1fxC*lL7ba3P~iip zs?qUim&25>X=3oI_t;{?G;sZ6JO6l9LFAid8QUO!n@n?<%e{;zzI5VSID?I4D*iD- z;ovivz@WPl7;=6Kfe1oikP-G!ZtC2;eYqwu4A9fsq05=Kd41XZQ>hw3EC+-t`iOpS zOe*71QHKW)Zcrg7yetN9V<<7C`+8CmM7dU2UoL$}Ni_k)CC1*k-mqzm!bKM)tBSA4 zfGvk?ej%!= z++m~7KVT%nw+Hu!-X6iW^8ove3YuENh)X%ZTN-@|E5+$MkuD40F7G(hbB`4Bw9MvE zrOWdKD-iQtNG9~O%Kg!9jW6gqqW*-70r~ei0{@RbE2a;t$w-zAD*I-U)fTPa3b0eu zZYkWID=v%wyeVOg8?ODx$Yfo5w-h*-q(lKq0-#Eh5^=wDszC0sUEWnFJAGQ4wlG!* z@dM@y*hgT}R1VnuN~=F?3n!zlJig@wdeTZW1o*Y{2Q|x)69n=hAFX8=Vke!O0XJW*-l!^|KQ5R_i1SS1UydR}VqJzFt-*NG+GdW7cNqP(THVCM?VHI z&J@>oj+wlz-<96fi2Aw2ns(mA#2lx-o-hH^SKHm~RW6cG1i59}<2>GNCp{`nPP=)b z=?3wQjm2B(My6x3jAQRUiXscJ;$#t9r-*&J?c5vSy;{3lvgi2}~xNBKc0*U;;0<*Rdbyzk1p{vCgNz$fJ@GXRS* zPZ#U=xzROm5CHF4cH7l0MLg5qADYgKe*Gm1<44$(QFDun8*oj{Lo1e9C7D0aB4Kyl zs-Ld>ZxLdTQgP+Kr9f@fXuC3?C7#DJKfaniP@C?~XQ775z{x6yX~r0wY+M_@jt|v<4SPzOI&!* zY~H=k_uStwa-l|$dtK*s_kh)U!}`q7=pF@9_e(3MORhS5?mWmJ=XKq)x_tru%kM+( zzw}T0o^+_KY|o!3kFnU}=lr%w6UQ?@KRJH7S+fvS(wnkm+{+`6H*3fTK z8rCBNrQ?~VM>VYJY*K)<8wR-e?%fgLYFoX5XYFZ(G7}u8(^H_GzyJ6uhicj1`z-_1 zzN#yWZ9P8JP)sBSvKzb1{rnof<;VnMZtp@UZ?WUM%x>&|U1RPp(ST zwc|#Mal5Q%HO#YScto<-DUWWr}DN*NWrx$Q5S!Q8uV^ zKI-=?;DGvALebQXcVtL34(NM8K{%0GKH;I&1roz$3CCGc7FQC3^(D*mYK4RJ3In@nv6hULkpi^%XkM; zi%4RKC$_FeN5ZHy!p)lak=EIdG_P(@MT3AF{=09)wYB=^bNrrvsp(IOK2=qCr|O)q=$-p5@M1M6up4TrqIrU`jI7 zwZ(!PO~<8uP<}>{jDlBSTlJcCdLIIFs?T_)Yz1Pdz8)H!r6l0DH3L+oe6^4E z|J3AmUE99%XFKrggUlx65GVv@@lV&)sY#SUSmzWEmcF0M;Xq*H>PfBnWuAvks`~t)ox|P0ey{k#Tiie=Fq5+ySupB2}#XEni61NpV59BTx;tVVV zG`pR>-uoLGU=-4~J%*tswC!HsNI*m5lu%8#rP&urE{+WUTm=_VJZ{9D5SVHFu zt{7HulUQ~c?@O6X-2#e3n~)QHfOg}-`xyUR_act{UQPS;boh#l_ax5!RIRRISK0;+ z7#rF@RlVM=$ckN90Huek6)k(9ooxSjjPLf5HjtRq(tqc9PSrG66W(T0Qiow*F{-=e zO9hzrPhv1xDBMM3mp1;UD$$x&r8|i#Vlg`0%>X~n@iIk?z2%b*>Mc`Voz>ie>>TdI ztQAl_{Y*6=5(gUNEOP$vRK*9tx3rr{eM6&~`-+=~#ng^=f^$;*a8w5$Ug(a@C>@0* z`6FkW9?jGmA32By@hU81JUl{sdVBWV3rBu~vikuwXSX4b>eXVg1St$&YH%%vYL19A zq*5_~X9HQ`hDxl3!8we}`osw<0FOJ4O_eID!I(Wo>ChYg{QiWOL`Vgt}U${34lMZM&&2cs7$t7AVN}-Fy4k%-T z%W3G(fF(@**D@|$SWdec2XWu4O~6<9)VQ2&m>(P=_o8nvOy;-J9lqVyv*!1ut}*ZZ zRR3?uy<{QjLf-Yfpf#^xE6u|BE}QEe${0k+p!-?gdUC||%9N}Vz97{wKr4_S3yL_A z7K0iwjoWj9PT;Aowdc!|vDn>*7en^9QgKQZoTmVqZ&$2rJf)a8!~?!+UZ4GDTYv;8XndJ0Xjp|!r`TPpmrLs3>8;x> zYrmIV&AS2odb5o!1*)`#W7yIXpSh@cxzJwGpIi4gs(v5Al-}-&oa<^Kb`<*LA>F4( zBNX7HBGk(z;Pq(Z${#r-YH&uExZ*)tHxi~aA&eCnaiE*5~3AmN1G<3AOtL}PBV33Q| zO^RF#1s6YLMFo#h?4Q}VW`!GI*Z^ARAj#?U#Zg*r64hKi zG7_Z&>#rr%U{Ew7ry)l=rQzj%;VJTb+S7ssn|BU;_z8b3G^~O)1gudF`I+x~c{_tR zWbX}QeNmh^HLi0F>nLitc@b5SXTf+)9oCNTkolJL{R*7{56mRcr-84I|5L2OCe#d2 z8N| z{B=Lf|CQPIzSXVG=g8C57epW7m6ye~uQ_?*4UaGJyBGZ4((~>#zcXs=`jsI6tKYFO zW!M%8YLJP?VZElc1Y)F)%zKr;6~&aE_O!+2bBhC7G%jn{N1Z7YFv?~7pjIby9q8>S zsMzC^B0_!OZ;RdFqJS{cB;jN$m-=wkc{NZ7geDW+75Xn`^G4y(ZeJz0!_Ty$31={2B4hHXN`#`-;M09LEQoMd0 zC7Iv!p?+tE?a7Sd({{2qZ80ZG@P`t3WzJKhO(jc~K9h$ryRVpE`(&PXQG#zAbN1`s zZKR7A;kGE2Crm@wv1*ZM2n%#rdOF;R^Z&-Yz0@Y`Oknpu%j$p|J4~YLRDlyPGoJss zjB+w7m^QMKdWlX;;jD3@fEdCFK7=r(qo>Y4^>}PD_GRqz-=||RSQfUO9Mj?*_IMKe zjo;Gq{7!eRw>|jvDNZc7m2;RMaKk33^98!@{R9bo=Gk`MyoaHR{}x%lR8mGS=BPFe ziG5okD~{Ktq8@HX;cT!YT;CZiwJOC zz9%j@p{pbxnu1$`IUp!EVl*E2132~AV%9IE7lT2X(KivWv$YG7&{#8wFK@e-2l28x zFMpc3iPmxsiC6h2Py#=}+k1}D1U#nLbNXz+LV;aey%NUiHuHdb2{m zv;DIWW4t{x(jYi`&oa9yMnZ6yoyTe`)b z21g{wqWaNnpW4`n>ENzZzeammxCCkLiccbtxrmI_&pcwdV$3k3N!Ir@r{8(tY=_X;`7Iqx!=RKX0y?R-6@_xa72Uf%^BpIhsS*!Oy1rmir`F} z=tqlXa*utY>Z9!jS&ESI7_@`mXB1IcoP-UpBHYKw56_y+sZEBg@2zPO97KDGrznZ? z&|*X89J3X(W`CGS@q{2j_7mUu1nxJdtfZj`t{87`Tw$a&#<~`Y^I*aCTA4=F5mesa zX0dv^NXN+Pk6vQx1xGytnu0fX1GtHMt?6B{r$Yw{#VR}>ea;{wo`{aGh(DA5Z649J zZ$8#D-@MO5{!QLuPa*4Ig0G+W4W+G(*l3dc-z|?vaI{_%U(Y<=z;~J*NZiKakx-mq zkVqlH-8#O;P54M|&~dNfrSbizz{cW$wBq5vP2)0&?f((>m0@i?Pq+yLcPZ{paVYLy z9E!WUI}~?^LUD&;rMMJ#EfjZmD;7NX&F}x*&-Z@GlPBk#-FId(yL)EenO$O4ylOnx z?S68|FX$BL4SF6^X}e6ehHNKjgNt%%%nmzG*mWc_)_Hu{cJ?e&ZqF`vE}VtOcmpB! zJm>uK_H=)ObiN(tHKJ;J$k3$X_}l&sCAS-8#6q^5u#6Mbd zV)i>XLN16}6$y7dTho2bXKNLXqMvKcul4k3?GN3HtL|+DcF}$={Sw=h%j@o^_jcj& z4BmYo5B)^Zw~?To&8UK1*h`o3p}8YZV1Ip>@Vfiu6l549f4_7?_D>0p0k7z|QX_TE zzxdCLqxOhD)K9POFhkr*botBE7E`BY46=;7-BLBS^U_^=$o7;0Sd{Uo&K*AJTzWXS zcoG*cSHJcZvKLcDYd+Lqif5qa_v&j|_>8A~65U+v{}0DmGw|(ZOZ0CG*$X#$z{3JW z9y;ITZ$FYo{VDlwz{NHpXiZ#mfhy-$GjcSn2L906jAbMDNFyZ8$VoDhrrhxqg+vj| zlMaP4gdzw|H5*7Y7i#&H;o!Ere8G*i6RAn)q?3IpTWbLsixhhA{!#FLsesWg71+CQ zWYKDyzVojpTJiffy#yUzhO-lUaTS`JF-JA+pBV$ftsFC+-k*PAK-OnXGyzuLC8{tv z4;bIrYz1DoTW{8{GGVl1*pH*Zi70xluD+3%4rABGWP|*xG$d_ ztx-YwBk8ZOsJPVa4M2Ns#l1Get`gWJo0P-%0b#4mW`JKT{wE^hfmg3iKNZpsR8rDm zKH$w8P8wlw{7y9&TDO?7zsD@(9PlzEn-5y zmbuGDB7IYHQwa>2?;FKw{%YJM)6%nwr0zeCxADHk6z~TON1@)G5YCm7sq)SC+(bn& zi?dd}C2Nu@TGjq7p{BI0=MLlR(x45QU67g-P68!}Z=n#@k>Z1o!|KvhSJEX_&aG~T z8PU;%eJy-gJTCLq*WRm8@pw<71`@*j^+id>PYkq~>JB&c1{0Y8l|qv$ci64Fn{y9~ zyt^J-XL3g4A@#ncFOS~{)hmFjKFt}G8 zBA?mR#jcLsJh^a;%^%>%X*%BLbR{)P^8$JASt%~4%$)WCC`b?G4Oxhn@D#?Eos=6$ zP3z{8eB?S4KkxtN565^pD3w_k{EcJJvykCqcTJ2_hdkh?YR>S1#KXJ0;C60R9k27s zEDF-WDEpVJ$Qb~<{fF<2SoZwYn{`(M`<;~4L}}qToxqxGMt`%k-6vm(*|3I4@^j9Y zC2Qu0QEUc+i{%kUUJwCFxo?cWBHzhFa~{pd{J;MD60@uYlrJNivBda@pwd8}T&QBWvy_%t8>g~lC;9*LSda*ExqoGO+-WoSdlxD{dnpSI5t zRRl;3vE4wy8W8I;N42Anvz;cQ;6{M0hyuHJfg|{!6QGeZR^q1tgL?saC(| z!>PmY;xJ>2X-(xSh}4wvCWPPmS$7iMBXaN=u^B5vA1okg6aQf9Lb`lUQTt5ux&AVn z>^JXS1xK4(TNL%25!rgJy1h#5?App1oz)9Bo$?U#Jm_$S6jl!j8p@bN%t)g>VhVf*Cq{sWSIAJ- z{L!TudG2ZXjuHOmsF!^^E%&B4imqdvdSxDfo^smO0S z<;v8GM~N!WwDu|skEkY(+;uHB8ul@|;Jl?u)P$cCaRo5DdDR>`==iB(!_z4WJd+mt zz^ZA%$=8>m%7aqH*HpRqRqru1;}}q%oywCM+d5L;0hQ*secC> zFV%@e8;q}ARbi?b1&+}K>mNZJ&ZfW*g6f^qr#QN!K9M|Ig9*;Rp5a5KLVueGoH+cN zLwtk@y7R$!fBBM#VX)jE#p-0KRW8aZRe>m?I=$N$g8gzsIW(}*-yKSOu3KjPy<}GR zg#tJ~2@4*Y&3EaJf5YtVkgRvI4pnGJCD*~e>UFj!6D@p<%3xu;)tL=cqE1D{9jhx* zf@~uXIgWqG)1@Hl+SUXRKvtXvw0VgaFI#L*BMWi(@94q-ALyl>S9A*t3|ZEHOI-b} z|5OktGJp~wB4_!>r|fHP{O@`MK`++@BS8%DZ-rg%0w;ed93y~<`^Ya-L6c0psI6UY<;LvQmnoHDaig|8%JB-#)Fc&C-+} zH0$0|4~Ri7eA~OMNmB*2zPIPuentx~8*%A|zPuf}(^Tu-daVI%W->YPcJa=boe@D> z){uX61Ln`n$q|c%=@(K2_|&SqK~;x9r6CPyQlDvTQBq3g!f=scNya`vF5ediTJoo^k`?q?U;FU{*8CZWn})mWe8V|; zQQkfilJRYWdOA$x7{+fELoGfkd~>PX{4~V*>9*=$kORpLMTxM&ViZe2|8vmJP2yv} zagTVbZOo2yK6eucT(23c>&W?!41>E*)VKHNlk5YL{4i3t8~TomC5e-%lffr3>BMfn z@jxT^w_P;gr4EvQA`Rrnk={!F!wDiP-df$y2CXvF=;`_8i{3qCENrO#;ZgYS(QqHX zk9>tJqVS7&xiVe$uq)UrD1zobTPip(1G4~$X*+#k{YmH_e;={PS)Pp1d@kjLh+`AT zfvGWzOeeO?s;DJ~!^khxI+v;aEgc7q4Nt!cdc}t#6~*lBI4jh!_AP0h zV2cdp&4L#{2?Zz=(v4Y@N47anhoNWALifX0{m&u$)(F;pr= zqg|^0+g@|NxXAO~kM(Ab5qdWJ1MhUJ%3;$#e46jCu(8_i>0-J9>cF2Kkrc_XH#Ma9 z^E&A}e$O_iSVRh|?nNgBUuVrX?y%e&O+5DA!Yj7}#7xXv4hnc|$yO{!rE5l7=#`bg zA96W?Larz{zXT&{dl(Cu;CLGRADdDAP+}9qX*m#K-_ByZR^{tbUL@)fdPzXvr_C;*dVCCz1EgZI z@E^xtg7>iOwHv<&78H=mPwx^Jw;u7LtOxM$xSb1T&vI|s6Us1ZRNqKZNn_H;mBKUP zp*RQ_P!VF(3A#<^BY!TP1;uD+f)n%?qgJWY+|Z2E>ldh-ripX{+l`mKP}4mHSX4R6 zjD@Wp$LJ<7ya>LI^Wb&Hku8?vIQFY?1jQ!=tEDN(LZH zf8!MLl+xkZz>fI#xj(FyP~~Otn`yuAZPWY_HvPCoW5>CF(RclD%4w!eIQST1WIsU@3uAnPGB;ou{e8nWF7 z+URgAwVIVtkt@9GxM3++&twg0*}(tYGP6Vh2=Ol{6fU1~0Pxp%22$mLbo#p(TE(p1+Zy!(_z z`C7CcGsM~C`mbPNJ($(d9F`|go5`LF>mS^ZYk^=YqU2>YTb!_RVA@e)4OzF&(^z`T z-vqSY(dNA7VC)89W@~~hOOl##0zrs{Bo2Cp6cCnDXRe~ADq^aNnNzfyQHld8@`N@G zzSg;S)w&vJUyu;QyQwhOHG%i&^1zc(s>U=g+H&i1eKq9fYh;+U&L4#w?Gjfj^8=)Yl8 zgieg0hG5?^SaPJFhJy%LKDxS2k?@?Ix&JHt*N4rX>@ss3U2BwYt+wdWq(MF+qA_}h zrc=vlZ|=aYX&4d7<7PhPgnK>xUx#hXKbmE;nh}oDZ#^teJcEa8oqPN zrKULxc3p+pbMw3+sI>jleuOfg%_(8bDPYjXL4-2xu!BUP`9v+D`E>bBRM}E zZa<79l@C|1U$q}3$}W4`9OKG~${B{rx`#_Px&eDyHWaT#f>U9q80-9PQ|n^iQf)tn8}wP>wv zI@wP%%H^4JM_3_Tk;tL*MZX991I-6WGC?eICzU0BzKceGc)VSdCBIQ__}f8Ozg}mP zn#$4jpBf{TN^We2SK`Q0tqQv^wUnjx?Y!h#CP99w0Unv){>`f)8Sz(KVOn1O&mX~Q z)lNY@nSH)|8(_D&h~M6PwR?hq6`)0}fnf%S{`<#RE~Tg1x?%~dRUtd}A6SO7bkR-f#V zrA?AL|40dp;k|%Y+++y17&4f}#&jtd?2I5xCU{)mrgF^KCZqHfGn!adlU5P+U(=u} zWl|zyg5Hn&4X`7=U+c!2NS)1Iu1VNxax)Xsua51B#)#7+Bqjlunl2si1cJIQae+p9 z__=VTU?5(@PIQ?tCC>Z}Q$y41keOMa$`l9_V36dECEjmrMCh`|iSfxsSdkjDAd7ku zCDNJ_8yz>CK8b;cV~Vyaa(3MsnrgNIZi(!uH9t9qt&gft`M$;SEYlpR+_) zojeo{#F$2>QDM?Wg{JJW4+Hu~Jk^*#ILVfh#L_lKKBVZ_|FZv(HqGK_NEiSoH>K4Q zG%?xRprx}($7~;kBEWLf5K2CHkN{9`q5SbP6itr}^ZtSG?Y=GA*(m`$s8mi5@dTPv zik+k)0_xj`ZNnaQcfR98{K4zN4tncl=)(Szn(}u4h#MrLm*udBh!yVfBf5N#$-;lo z?i^E)u<%b=h<8l&@$qqUwl2uo5eq;`DXqqyzV#xcB?YzU2GCvC$UMNO}43C%eN!RkOEV1RDzL(Tq$+JlJ&P+10tsU z(jOJ=6S`!QaV9@o-)U)hTB7{co+0~SHZP>Ws+K!FEU~n(X+#WX2cJ7WrCkpWCz|<) zQ7g0@%B_5{AdCLv55?xD^4a4pkd|beaM5vTy^Gf48K$$HV)9SlG152rJOSWGiH0;+ z)m&H`_6BBq?N0OLkv06sPSY|sG7LxYj!nIjM{40#tCxkOhh`(4F6SoNmgb}}RgJbT ziS#{7?Vti<%4B*>JsvWS7Z=&VBrluacly|AdE&pF?wU5GviLNR*!lR7Km_P%9KNcO zlub=^;Y?PHPMMciWxgUzf^bM?i-T;Ib_{O>?2GWE?zE*Rm&<^9%kg`o@u_=hp-%l9 zkCviZhd+=n!nH@ANCP>jfOL-THqtdn7_;4&ggQN2;A0~tZO;8-+gSS}pQ@~%I$Rq+ zhFTz3xPLtzm%3P->@-i#7bqzsUEHdgmEL2xMR|BOOR}NNe`Z#)29xa}vVrdiXrf!S zzCTB%xsHue^6rY57zyY!H9#SEB%7u+{0B=av0J#^=Nop?hS7ny%{ z(fjVBK67}e$BmM{Us!8Y(f&gXZ$17{6cVQ^MD|gzvrpc-AQvQ@TcUaJH+9)9 zuBoH6+=5@Dj)bhqL8Ox3SuL`~XeRPbNTXj;s4u$Cr>5?{Q)nx&GbyW@tjMc7`X}w} zp1C{;mikF^SWzk0-c=TF^9b>Q5(zVXA#TcDR()NGlN;QGvFPe}awsji?$bCc<{r3^7GH1d>a5gZduww(wu!c0 zT0KYGj-nJ2RBL~G13Qm3z^SEL$(LfC6)N{XcwE@TimB2crxB zME;%>>$LQQCnj{Tp^P_-y|R>yLqof4r;yxkOTZ>`qRYD9VBK;6aB=q2Z%&6IbMufR zP~z_XA~3zd`Z({ud(Aw@HHl|?Wk@D2bViI4f^aDy8ivMnz~PzUbr6HvQsZ@yRT)VZ z&%6W=<6~4%pYGzeWx7XYC7*1$@~Oc7kp5ezjP2a~Hhd{s%925Va;W8;zkIUicuQ+u zE1m*J6Z6G&o0}29H=&a6=9JZvd7oQ*QlYM7yd0C~kkXuG>IF2ucU^&mv@dkWWHig_ zXJj0fXh$K&nKdsK$e==pCtpZFv^c4ff}C5KF)e08%!-6oRw9Ssau*63n*QX2S*|Cd zjz+c5m8GchZWxAE=Y2WrGMVcb;Muq-_cL*b`zd>cj3A6L2dms=xOL)t#$V)0dvtZ~ z0!9!@bo1@`YBNKq;R5-~6R1w4!D~|)ON%SUGX}MZ?gNv65Cf6M5WNs;DqL*jhx6F< z60UZO*h8)eXzds?@BXJTIuVovqFz`k`vdOvO+lO>-XhH0zBKgkHY7O9FpXF~gC`%I zobMVm!bJqDb|n)UW7e^@Fw|7}9M^g;?|cT9nYM+|zO0GqSuQ);Cax%q)no0dIXFtC z@zZ6@fu2h<^M9?8`x+O`;y0FGB!z*fVpLeK#Rln|+mjMGiV1kxt25anVf{+uld1m7 zY|MpK+?J)qBYT&X+?BkJs1zOE-j{Xh)>4CRyOl{OAl{4n!{)}iamjLng(%R`)EqWRnxeyibzAG|Q;9Ms%iHtWvA z4UO9B)6G|2l?PmD6jWx_k7TaOAa+2v^OCLO)2`(Q{k2>UjtuOh$qg62#7ClC z&tE;vQrXt+l5SVNeNffU1px7+ptpvK1=Jc1k5C7<1iilrFJ7}?)_d(n0nOKo{$iXb zAi5KMusvf>aFh&-{iLRR1MZG4sbo1(9`aY_%{`Nnf-H6iJl|vcwVlqP9&JXW^?;S_ z?}DG#vSU2diPoJm(YmrPFVV4>+dnn+dH$hTY;e*ZjWkjkPI{P8nekV?u>{s@9mOME zmN;DmQNa+1wRd#1=#LvS(N<=0;{fAMJ%hSS|K^h=B+*uKbr(1EjWj%D9tJtEBtf#% zf2L2_hHJ^EHeYLRY;3H>;A`aIB;azH1JgcN^l%RgF;Ts_@Fvk$d7^dqfG&e-{%!^) zIJ1{!cZfUCyLsu1!wW!s&Q@l;^084TPRvq_2T+=2!Qckzo(8?-W$$i+W10F>KZxD|LfR0gJ1F)9)(Y-VXus zUm)yWeM2;vzLY-RjHR2Ht204bzjUDu|F_E?D5$jP1fFRqCcn; z{1Ec_MHOv&h2OjTXeCGbZ%6gPmr{)~HyXcER`pRe&QeOx=Xtx4)f{oG{NGyDbJ+-q zq=I#V%0^e01sbP`vPo40A({(wGaPXmhQp+^hRigerm%rOf()KaJno)1o?B%`K&qW zAw5F4op1*)6rca!R9PVOG7#`Ts;q%qg%tq6E&P9}vU%PqouIwg=!l_Bv@+oY_639g$L>f$iqRd3eUJc@S4juy3Va@b#mhpe- zw7;sf)~qfbM)b5+csc|-1RMQ=rnoSyJiQLj{t90TKTNWOe?f*1OU`tu_X_Sa8APW8 zpvu4ufP(uzn`8pm36M~60PxfyrZBxGH0+81f_RWjbTe!bXTZ~~NBMW<6dFak@Kj(X zXu2Mdf+&t%`WfLHsu;eyETP^Au&@BrLp?SS@CvY{k*Z9l`7+>IF>~@6%8k_TFh8^Y zj9HBe`pCqju)}xbASRHoH)@+YO9=Q}^=ihRSclQ87hIJ_^IE>JkRxb*{*6R~1#ocC z*yyuaAOGbmREprH%BD0fs%}LBdg@_lD3HAT{JUM2=HGE zDr#$6hE~bN;9yks^z5cj{w`sL8Uq<(e{!M9c-VkOU&CATm)qRSsnZh&H2`B{Wh~o# zPYl7=nfy>4qF%$6wsyBaJtvtRiZg=>i_NTn+nxBmLux&IVMQWZ?zui2(BiomQqTQr z_`$KUw)n8OY5+#HF7~h@I~z7i$U%+anzqbMr5aA~)!qnk!tlHwQeT2v8%0==lF|H$ z%lU7|wWlAh^%aO>nhWvBa@fWQs)Sc@90aS@Q4F)C+}vvqQcn_C_`%oLjPy7XZzvQj z#rS6IyC^6Wc^!TaiQLJZpQU6MHMNxab1<)gI$$n>z7$lKOD!#BeWE{cQi_1F@p1!s zMqiv@#xF}yMg|FN>FvSTnC+6(KNu{mVCXk(9Etp}NNZwFf{!8Psc*j;+*Z+&`Py~^ z0BLFJlVz!$aKYNQgQzlQ)L0T3(AB{!8m&rY1t2wp+;1$Ir$?>+J10TBI0?Y8*kYv6 z3`SK`Qs zpO{=Zr9r(mioU458nlx`ChRa@Z0ncFwRWXUd4V54ma+aw?Jf@ILWP$U37=N*y4<`n z(>X$v1;{Iu;~~Lppz+j4^+m&Vr6M2r-TNvSyqwx3>%U z)or2vMA~+{S_*{|4J*R_{F5TH2K6MT&k6tqK7aSH76ecw8G|E4jl5Qkl@Tu-0d6r8!|~@mHsron z21zi=hq=e~nMlFPbGXiy2wMYv^e?xZ(c~s@H8QvgmhvW_1!puC(Ib&?fO1*yP{8bFE~z^f~x4;QQD6{}qe5grQD21GJaYq?U zaDp*8v$&}j!l3sK>;OtrmU_xF(OR$9aex4o>eXnG(^1CVRQb9Uhq^V=>C?OEljZ3| zp9oA_&R~`|2a8+a^dDwmv@mN13VIY;%DymoLPnVN`H#cPwe?MM{_bYt)jxNY-kV=N z!=grpmzs!YUc^vgJp`yh;sDb;kZS}@a}h^=;S%HIcfb*b664efRLlQvs=Pe-$$5z z>xkc2b45-l`3B8MDm|~XXXqK)#4VTvNr*tnA7>RmSNMktLP&ti7==>eVj=R3p#RnI0+%L_akz7uR zB?3HPRLcfAgj^81NsEtv(b4Jno55XlI$#Ya{fUjnP&yZejMx|KEF?kWkCMTBiBWwf zfGIA@Y0NxsZo$S^!coj%dBNpRCPReC{>H!~^p`d5+{hT|z=9$(5*xvIS>tz-T7L+s z6uA;+zPB|Az;kRp($<;z3w;SPL+_^*5HF!H$@NgF7) zuGD$5T|7Pz`5Vuj&BDGg30gr$G2Zj2n3u9d%f)bbe?g%dtKiJ(gCpg zNg>KT?x*9!2RnxcD7!ph*+$b-qxILbk!7Q$-8q@^Iq~v2SwqChJbw5VRvl09#`HAzd)9I9@1;u@;M|mtWb^b!D&mA5?0rv;?+@4B#;$Ro`Icf z5i0f5`Na$L7NkNapo{m}J`6x)B|!BmF|P=YH(&`O?Q7e*6RHmNx)aRh;7lx3y!|Rn)r{9i zK2w!|v1|Ct$O<&b{cP~yed<@4;*w_ADUOqf)(URmpR&7#G z-^|7{Fnw#Oknb$A1%ii7MwI!W1LF5H53Y-LD-IrEZ!Rt7EVR5{7*QP$n4}L1R_1f= zyn8kFc+`R$XA6@{T!3vPefHbzqk;L^b~jQ%-bs1s zl*t>dn3Qk~o^zRl*`Dn~gg02faDkep_&M6$mZTb)mA0UMh07q=jAKHG3>jaL;w`98 zzw28?PQ`Ep;1fxMnR>N#hm+G!PEtYd+;5Q+8Ti}j{I;JL{Wdu>6#7r?y9Y|#-{Ofc z{7}$E)q`>1pg$U_1V+!gzl8_&bPy_MSO*jHnaFjAk0hqmbuxSfX6Eg-^&46nTL_RY zD6gZ!z4@!qA1aZxdF7{&3KGhow@k_nm8yRS54HsO9QvU+)IeDswDLN4Cl5MZrU>b* z*&(8@ZXCBrzq4y-4EqW&SvPJ_txL32!TiXfE`t@ee z8TulYoOZAiGO`JQ!RWqagg@JaEuio1Cqcr;{bDhO{4$!__4r*FzwNENNYHFaoy%2S z*z@*lnM?B7@*kd#i0#>*e|%m#L^@l$$P?8hOuSpc(3#mM@NP+`-a=~~xzF6$KP1Y? z2;nsNK`)iQ_l4Pe`{>#52hVk?MUHDf%~V-Y3!McOCNB);uf#kKMwmArH~5 zxiD>S-JKK}ln@-1n$_sEOXt3{qsbx14CeS&?QVAm~{oLx*>lB)~%pRRay0 zA~D*$Gi_#q%PX&n>a|GW>Hv;whTnq;0E==TEtZ+HWk&e}*UNrI175BVNlqt2OdVC2 zvPj}R6Nx=NFNFDTQ*`T{FB%5!-CK@p8sjLxdisRl-}p?njdf^>RB1yn05{`qiOM9z zlVeHf4sQFyxmX02iCuX2XkM*$LU5@tL8(;rFSf_=r1QH7+2Hd zr!H%><-4)&%F*owMc^@6eU~YBd)JDNt295 z@0Q;~z0{tVylAsfE3A25B<3x%Gb%(ysZ8}}A3W-OvAbTCs@FM(yzY?~UR!(b-6gfi zY{_MHJ`5@U?K}>A`B(J*g!=aLC+>r`Jmj3_4o(#CVs~Yc)C_e-a`L62b3K3?VkXdr zl8fkUnGm`$nmw8MY@A*_fMad-w2E%T3rXmnC)b?WCpLm>WFh zgpl%l|2kLRz=zt0>8}+_9w)aR8(%PbB zV&dp}KcEbU#b=9dZvLoA%)6$R02}6zVmMz6t$v-lclX``ICZ`&y5{dbdq%T}3_gTw7vh0y#)(&>dSX;9#kN@k@9UOY5A>?t|mMOZ?NT(j^?8UwNJ4*Wz z?M3d-#nsbDxji))7<`17UZ8V!^_ZP^uA_ba+(L()O@`#Nmi7Hx^8U|@B!7+atSd<4 zFah}Rv2EFr2cH_T}DnE{>|$~#WhqyKPEsw0MPSx)Un*W)S{D~ z8C)8HeP}g{$TEA71RgVzHvQ-((?yofFvL}`5EXFYBi=|D++jY#b+=VlF8O_2Xo`); zB+!+Nhoo-rB4J~B^MVnCRvmTKxK&ip+w&0s7@ROm$eu1ZHZ!v0f*xN7Le>F9e zF+H(}1KHm)^VBk}fXxnlN35J1ezO8c2^re@5kb<_2C1N**>kO zt;Em&cTr^N+rUf-AU9w=v-M(`5~TB~FFayT$>%)^xZd&;!4Jdlt)?9|%H_#3si=vM zIBBB|^#e|4ZG_I&dJOSp(46!35h24Q3qW~1`4c%LEMEB?Xp)m5XC4!O2c`{tqkFB<`Nu^s<;!tKQn8sC*_-e4W2VL~UTEYZ>-y95S|IP` zUk=q=1oBX>ejc~R9Z~g zIVimXUI0>rs?3ylrHl{THQ%|unWMiD%z5Hw;HwLXXbuv#U6H>4rgtoPj_2!7X1t|_ zY@AqwTN3jwF+6@vPT81jBh_T)#~+N)nI0qt3bmIaAUw;~bY)Nh;%*owCo@cDmqqAy zMCPiWJA^;RKYnkl|N85wW+*21kQ1<2zZWCtg3^bd@wFS>->ov*GGY}akQQ={Fr^uI zs5B|#nvPt~G>kZb1UOy3N@yt^Pe3Aogq=BE+}Xgpgm1^fjm4M5BUm5;G#6$Tj`-tP znWW3Xo&^6WI*%CSv9+8Wz8|{1oDrUa5mT ztq8v;ap-(Eu(VMI{Js!9yIPU6TrpxMMD4x6zMA6pBrC9{I~~}R79fMF+~;!nk144j zYJq=UHClo1ifBra_$J5`3KzV<_`2?Z?R{bk$!1+LIBh<1VuA~ z+k=%X6FSYMS#a-IpBMpE=23mB@DpfoWc#m|Yc8|<2Dx97_pW%GpZ+4E-;+=$(QyTf zcY2=rcZPYMu@kt>+!hx`J(IRVjC1NM#y093t>{meW7y{P?|0!Qy5I}zW#%xhXIRQ(l?yVK6D1HxR1&Mnu zmpqImiQ@kWiu;Ehpq=L0T9O*3TXa0<-kWO>ia`{1iDCjn5f)g!@3|!$>9#zmm~a$jB`IlKqmz>p^6n>rd)$m3#rFAh~WL{ zH$HUHI~1Y$NBm)xPJk7;>V(3lF-tOI>VOytB%UKZH`<~8`fHruLYfi`eCRE#MVDcK zMdWUt<_yO?_JC0&848|*EB|zT>4=Bb^LWet_NYW&lb;-_P(~v@#R=0Bjk`Q84c5bn z;vw%^5J;quQ<-LKk1I_d%g={R&v0w~ohwT{(lX5okpw4n=QA$kAA6sI5{aJU zrXwhETXyK{I=6Z+0asm5l9>|MkgO7Rx1bc|PaOmgS(yVv?LR*#;GgGy>xDf3j@S&7 zhxvKd>N-*ZxsSq72_{?t#f`Y>82qK20yVAi`LCMxmMO#j_B!281;}BW*55*7UJ*2M zMpK%(bldG&KOw%Qg#Pj2h2YBs>bG~%Mzgst5dsJsudB^H79<>#AVG#8L2jGxA@gC6 zQ#v=5ez(aV%U9+>=V3~ZrgMv7=SB-T>Or5}6NJv^EEGljksp_K=uo#sFGOLfLv`+4 zQ0n3Gy|30Ci3u=rgvp%OyE9_GQET~>3nCV{gES39Cy)w!Cd*U3Sfh}mdV2Z_1^B3_ zroaVf5adr3D6oR-Ng3S?PzC6>BN^H5syg2}T7M!k<)jVU>+~#Ebpd5&Q|`*bVdEva zQc9}K9Dzh^Qo&CmNcsY1>dd|{xzMm#f~Jssiuo>#6mq8~+fM%FP_Sfh766rcLC7yBTWh9iMJ&>X~N#`pbRsD*0TkD zB15SrdAHLH3qR4F(okimxVIfI7T=E6!A8A1Kn%dlOw)RxL-v8N9?2hhd(H|86-dg#Zv6FV5N7o#8Mvy!7|b0_^V-BDy*`3ab!n02z|P zkNv(LRB=(}^ED~_o#(R)V+u~L868qL`;G7Bn>GT@fPMGGnF zl!-rHG^Mh6jZdgU6zTFe5k~{HXlLPr)t+11^wt*2uZ4f=h*o&Vu4_mqnY>z$_(L<8 z!T&@;wX>0+C5~Qbz%SczWnic#6U2hKkfjZHS0bnypPdu%w%hcuYR^guT;PoTj}F{h z9g(Ac>ei#ZJhvz;FA^F-XyXB3kd8p|0LzHmC`WtHjVXPU_34C9#}ML3G?DRRFL}b; ziO@$-H(y{p*?;fKZPGFYJbl=5z=~#obCb*R?0JXciN{wOTH25!1cXjmIS+A0E_wfos5^Aip!y(A)71C20w(Gn$^bO%Yo{kb^;zZVW zS%%R(!w`(Iit5wKO1LrEH(54{@cM1z8x%wM&@4)hWAD+!AwIFo5mXE5wGXz`8J`iw z1_0o%Tv1AfZ?Ibq$|9VhYy#q!6LiuD>v}u=kF>ip4}6}&xIvpA%>sTU#WRm=42x2- zd4;^6Q^q3vj$59uacE_v0N`jc0rpam0tNF%ii?ZL4$6RJ4~r$+r>vLUn3#D`EsP&X z85^5WLMCEPGE?x|F&P*EXj-95`iR|UR3brHg5d8$kr$CVq|2E$V+ zJCbr})?d4#Ujr_t%B4wrW5!^CJi%q z7#ONq-FuXmJ2kJ1>G4@!K9nN^9g!Cn`Q8u@7+NtXXPWaNQ%Cn7Sr>$Ky^Sy8m?GbmSFvI@n+33AW zs{gXAonnx6KvZ6dcW6LRx-&>Db}V}&1SF>J4xD5@ba`MVI~h3CeBeWJ+DeYR?1&1C z)wA>4^1=-*l$4R-AwfrYdVbz^i6@z*RWtG20Bl0fB9BYr*qOwc;}8+%O!_sT47Oqd z%OUXoWBG#_S{U}nBGO27UPw*d@p3J3_wE&RhX+jh>qaU3;*|ca;O6{d66OR^a>*Y2puw~2*fAjYUr}U3DIvUH}k$cE-36?6=BB} z$P3hw*=7W>BKb6~#1btz=U9~&QSdh{(D>zE^ai!$-Eh9FBv2sA+WE78p>UwkT_bVn2)f?6(Y>+D{npZ~wd|kE#UnkD!xN zA2jy;#TQt8Sm?cmw&HM3)p}r0mNfB!y5Ue&U37?AkQO=>edHwKn61zP#0!Df|G-!s zeZ8Nr5i%JbRu*nDB6!sy{e#$G(y}(aR)T&87(kNNfEfcXdAW&{rtE`hW5{H3}z$2vV0OzF$6CpiK1 z3>B4O=v!7I+?31W5Zanbv5IQQAO^Z9&BIbdp_7aHaMEMb}%=R-o` z(sHG}C|9Zb%Y-l69}N=34JVGcP*akeE^$dRiu$50oSWZbH!k*rpYzs7T8f)cL-oE2`tK#HXe&Hr|XP433gSG~vuL`9w6c z<;*WI1>OFnVr1lx?p+ELL$KGL+Y3@)kqCx5)bZ~TV=(N;F+dIT5hW-yXXYIB213b-e~)$<|C;?%NKm<8TkX91+*eIKva8I;KQ ziImTq<8lkhpGe;vVb=PR@@Aof{C;%ZnzHbexm&iec9PqdkdG45t{O846y9wIy1L2^ z$J64cnfa*pSrFxE~>8Ep9xCBVF*DwBwmqHaR^}m zDd`lY1q7sq9t2^45g57@BnKo^1PKZ05E&&!0i_%oWPp(nl<+;zeeQ?*<$gWq{Pz0q zb=KZ1_F8Lq^OEkB+7TUDycQeHf2a;LWFDxjEPr^*Z<5Yh1SKXoSmsL^1Dq>LLWm3I zH$$(Qkh9F}yfS4bQSl;f>&xwg{X>y9$JK`6jBp ztNxloi{vt<6@B$>CwpXRvafQPd3a=Shj%RH7-a32%!RNi=99VlgecpmHdjhNNIkow zX(<9(rVcpMQaxH(tj-~3zg$i?4}Tjx70NFLX|C0Xy{MDsa;jQYoWLZQQ~db1Hp8R= zFYdwpbKC2!zRHKS<}Hl}3y+f6BS}Vy4ej}>>VBRaCRt1U1*Y#lQ@_tY*8t8-b@=B4-xmvZI#=lxMA}lU8P&zY!QkS`cPb)IDVwU$$@+& zvA@wm7o$(;=Yh~T9u6gasXfAAu4MzwAyYugK;Q?dq&t++n7HJSRO%=&h73Se#6y5nm`y|t()nW<=* zqC02~=;$_lZt7faPx-n;Vg7jPfY?{pvaBDkHs|}LF=)^F#_U~{Ho7;y%Gd@Q1qgPY z9l28o_>Q+~Ou1S!f(!-7Pp?ZiYs|<+iFYMuQVti{d6;I_0^l#N#(KT6oE~6&us`u@ z2db36>V(8~4$+>uVqb>0m^0>Hpmyd`KpLr>E}fF#TA#gM1#!* zWn%mP%DHbIIa(E?O`Kr49x1GHkq+y|+HI`g!_!q?H~i=i>LCmuS+fUHzWy{l+UKT? zHXa@m=^uHC_W#{%+2s&?espuMeXQy)RGIQOob#(`5%yAzeaE_~<3p@E3|!bW5eE19 zQX}^q&ZwkXT<_*H$1#ef)l8nKq0qHdJGz#uDzwDf+PQ6S6GK`6h}&fEoTFrzX2Eu! zLC>8A)Nz2$h{lMdO)I`=p|>)*;-7`-d-p28!;4iXa+JP)l<}~jbvc@94utpw-&ZHzjNZ`WZHu4hUweh0;GFuEvMdDwTxS#^l(%qo*elf z`uAc-t-+Y%fC|Qgh}S2Uxbr**aZNI4DL78N>vF|fZH>t{=9%+VyoIxsy`4+6~C#I(N|B?o!dU}~G` zPp3u6eGj3zRfKw;lw8=I>e;WNj^E(XjA95ElJ$_dbz`7zGnGwertR0~S76`E+k#lH z4x^t2jtByDV>g3y1C?l)^#OP|hYe;jH}kz5+?xDJn|p@G}t@2&W_v-yobpp%*! zIZj130_+@pMgR~JKpv^zYsQ+n90Y6yKsTmB!@n$W!af-bzK=saS6)+u8VtZWQQ40VzQ&*#nh2$MXzO$vn5$Bm+rmg&(a`A6iS#=z(i5yK>|L7E`B+TrMPd~vfd>NME+>S}DC@)-Xzp3L z)e_(~t@Ns%t5gr3IL=g6*tJ)hBcI2PE8%YBhCCxWoE0~*|>tO=? z@!!)U{4M}MnzL}TZwgCdg7CB`wJ(CPdqSG!d(hjzt*=K8RKt1++qfs5I&yLxDCs=M zdyPk(i|kw7*#mz#8JfKOi4W11eKvjzAX^S>w;1(ljw8wuyUaW-yzuWqNH68V;+8x| zs)IQ2zG0=T`z3Z4@<%^lRF8S&*yT)74~Mtk25x3VF-XQhj7R)%HT&VB6dTs{DN&NME`=2(+avG zECjz?<^SFVQ`AmSVM}uhqzluNSJ#u*(vvr}Bc|Tdslc~(1T9qlI`rN+DtYlj=#&P) zbSnJ$b_3F6{8?zEbm&5UE1yrgyMQ3dg^p!9N>yCFD6_;tbSbWOr+A6-X24X1WWDhc)iOI%SZnMa+)!_QK$dRrxg zb%rcdJ(R|yKHKh!CIL((i@5rJhUSi&an56JB;iR^lw&cre2VZmsG3!?gDoA;hEqpD zrKFbp7Hgs9Zk3g(I9BdpfpV;W6Pxe$IZb<8u{m992-mP4jf(WscZ+XoHAokGWxo&- zp`JW;*bb|Au@msuTkQx?*g5Yz0JM6OY*O6)&V4Qg6tG*>4;NpVK2Nn~>L2@aN(;w! z5vVKIE<>mv2YFcrQdn!6@u|}fzb_>7)Ajv05w@S+=xO5n`PQImOcp{97~>ZGQBsnu zn0pMV;g{K9S3h|?v!0Z?8x#{ia_1fEWd&|9j}-yug_OGmpq^QGtvf3k=vp2$M`K+< zqB?Z8hh$v*wsIN%;iC$;0nj5jc|-PMcJDEiUp#eQPT{F5T{;Pp zGZ~PuNt&M|Z}n5xo<4I!Tp!C76G z4+{=iG3*)*Lj6;ilG!eF{Rk8|(P3##oUNN*Lka4g%?Uzh44?QGw0Z>HypKGkhQhuw zC-Rx*KdgFKt#W~H@p2H?ol6Q1^j+^bq7F1|h4YRwLrkkRY#@=lcrV90r< zBBdWMoEfIB^9;xd#F5JcmPscUKOivQj<2ZHRx7_em5&AEznMv*{MV!nbh`K@Pj!>C zmxhu4@|m)$Bg{3*q6P+wa&nJ}vIJt+ppyYI%~y;o^PNyX*V~4z9<35|Tyja7gNCP? zRwajf1K43%_v<7bWs}uOe&=wfsSlUFdt4{rqjU-OI$}Jr5His&z?cJ&mg?Bd(o00> zgR9m+-BKoAIzMmB;(RMR-p+1r6Q1%9&)+k-oM#x@gL^y>v>f3(3CS%KwA{9rYLH0Q zDgUioqZjCYz6f6&$tM41pHYa*BFCr_zrufzGI(7_jB_6~v;8fM581Mbew|q|p#5pa z_>+@nS8+q91^HCAYnjVfw^(`onAMLIAMr&pApb@ag=FstqHd}QsBqQfeU_mYJT@!bD-+dlsAZbhg5dfelbj}fV)`>aXy*hLcWyYJ%0|EA zC!u2j6Gm;ycp<8vu*jr~jK6!;wl0PI&hNpBG5**ouK!aUK>7ec3q|b}UGvpzL9*7e zbEIQJni|ttc2*z}n&O^8v&fV%4vyX9R~!-ug6T$kI6~-Ahgu(o$nR7FLf(8Y&YY@oh9To`yPrS_?@agnr* z#2u57!|us1z0`mH+9aDkSMGY-vgyq8zvw$Q{V4EWSCpaXV{PUI0FT*_D7%JV&1l3ZNgmQ~O5P8l zacBmCkck^h)*p~SjJma3y`mPtP(M^XtLKztB6dII-+2fk48=}p6=Un*#Ey=RRw8lD z6A0+_*Vo+pCR+M4{pls|S~@k-qu;&s(J~tw8?DP+v_E?uSHpFC=({@jS+&YZ7Jf%9 zF*4*qfmmDmZhe1mSN&!|%xm=f;{~jfZ|cRnBC`=|3>^nazkAaNF^kl4Tjy`wXlG~V z$s9RnwbHCYzFdc?k`|kUuVGucg=A0E%2`dZ%DmiMrRDQ<$}2me0D}-v)Yu~YtMI5^ z;nwJPCs2gD{Py}BDKSXg#6)?>&Mu>;WM3(HW{ls^ieY?K_#?-haQLlMJnW>qq8Y@+ z6yfgS5qd302EM;1Q6^lr*Fg>+ldjCm@?EX=36_r(UNSDxq(9Y=WPW+V{wBPs>0BE? z6#BaDk?!`RU@b&9p(@xp=q5HIa&7(6Zrps_`&X~_vSUunMxp3+PI0t=Nq2M{t6EC| z(*C|l^^-8CPDVQx!f!UANaSxp{9Jv&>M-#ZmE{E_yP)7F9Cy7q{FbLm-3Woj=CEO= z8^VxEVODE*J(bngq`#k;IW&UbnN5R}X#>XFB(H38lo@s1cX#ngQ_mA$}wX49dp0^X4^f4B|I1LFcKWGGHz^QF!VnWn& zrRW+xv-A4n{QLzu6&Wj-F7t2E^HD%^XI8%!P4`gTgLbkY|>8m*=~&ZnYC7x9tL--d>U zCTd1X@!-{nPEJlwB^E9M9trs-B7=Hj&F!Ohq;1dg!;i@6=%HgmSW75>KyF?h$L%T` zInY*|nUND`)lh|Y1!G-gv--6qub+^+pNu?KDlYjKw_0&l$0M@ad3i3T@9}`NyRUDk z{)f!}bd;Oh*z79f$x3A-1CFBdSs8g*ED?fG?k>T%Z)viT8imjSI9Cz|0kQo)H@AZo zHRAt=-0^Wwy=2*IKvs()%%pvMw<60voa(s2liq^9K?4B({%4FI+R0lkpw&2U3L~VhA~|9ii)Z@)Rg7w@O0CpSu9P9=^V} zCH02o0hylme1@3xQInfijL?+iU@A(5MM@8O3&VTcKyt747onRTri}hB zj*pxN|4c}XU6a}vSTYlv`9wkCvwNT&TGEasO%aH;@lEKCXBNiEFbG}rRyf@+EPP*W zt-Qo&$=HSQ4+zLqOvw|`Te<>NKyp&J+Q71l@S0Ra%M-J@iTTmjn_E68UeCBCA~%G~ z(8|i{KmFWdkQ{?}3)(b2KoaUMk8{yTV-b9MRfuaIP0tHRWtH&YXaqTN3XnHX%q(EG zRifWKm6esloGJHp-K_vH(05qFp4, , ... and Linux filesystems care).""" +import json, os, sys + +root, out = sys.argv[1], sys.argv[2] +dirs = ["crt/include", "sdk/include/ucrt", "sdk/include/um", "sdk/include/shared", "sdk/include/winrt"] + +def tree(path): + entries = [] + for name in sorted(os.listdir(path)): + full = os.path.join(path, name) + if os.path.islink(full) and name.lower() == os.readlink(full).lower(): + continue # xwin's lowercase alias: the overlay covers every casing already + if os.path.isdir(full): + entries.append({"type": "directory", "name": name, "contents": tree(full)}) + else: + entries.append({"type": "file", "name": name, "external-contents": os.path.realpath(full)}) + return entries + +overlay = {"version": 0, "case-sensitive": "false", "overlay-relative": False, + "roots": [{"type": "directory", "name": os.path.join(root, d), "contents": tree(os.path.join(root, d))} for d in dirs]} +tmp = out + f".{os.getpid()}" +with open(tmp, "w") as f: + json.dump(overlay, f) +os.replace(tmp, out) diff --git a/cmake/toolchains/windows-msvc.cmake b/cmake/toolchains/windows-msvc.cmake new file mode 100644 index 0000000..7b5bd9a --- /dev/null +++ b/cmake/toolchains/windows-msvc.cmake @@ -0,0 +1,30 @@ +# Cross-compiling for Windows (x64) from Linux with clang-cl + lld-link against the real MSVC CRT +# and Windows SDK, laid out by xwin: +# xwin --accept-license --arch x86_64 splat --output ~/.xwin +# cmake --preset windows && cmake --build --preset windows +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_VERSION 10.0) +set(CMAKE_SYSTEM_PROCESSOR AMD64) + +set(CMAKE_C_COMPILER ${CMAKE_CURRENT_LIST_DIR}/clang-cl-xwin) +set(CMAKE_CXX_COMPILER ${CMAKE_CURRENT_LIST_DIR}/clang-cl-xwin) +set(CMAKE_LINKER ${CMAKE_CURRENT_LIST_DIR}/lld-link-xwin) +set(CMAKE_AR llvm-lib) +set(CMAKE_RC_COMPILER llvm-rc) +set(CMAKE_MT llvm-mt) +set(CMAKE_CXX_FLAGS_INIT "/EHsc") +if(DEFINED ENV{XWIN_DIR}) + set(XWIN "$ENV{XWIN_DIR}") +else() + set(XWIN "$ENV{HOME}/.xwin") +endif() +set(CMAKE_RC_FLAGS_INIT "-I${XWIN}/sdk/include/um -I${XWIN}/sdk/include/shared") + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_CROSSCOMPILING_EMULATOR wine) +# Never pick up the build machine's libraries (e.g. a Linux SDL3): fetch and build everything. +set(CMAKE_FIND_ROOT_PATH "${XWIN}") +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(FETCHCONTENT_TRY_FIND_PACKAGE_MODE NEVER) diff --git a/content/keel/cfg/default.cfg b/content/engine/cfg/default.cfg similarity index 100% rename from content/keel/cfg/default.cfg rename to content/engine/cfg/default.cfg diff --git a/content/keel/skies/CREDITS.txt b/content/engine/skies/CREDITS.txt similarity index 100% rename from content/keel/skies/CREDITS.txt rename to content/engine/skies/CREDITS.txt diff --git a/content/keel/skies/day.ksky b/content/engine/skies/day.ksky similarity index 100% rename from content/keel/skies/day.ksky rename to content/engine/skies/day.ksky diff --git a/docs/content.js b/docs/content.js index b283f51..3413dbf 100644 --- a/docs/content.js +++ b/docs/content.js @@ -7,7 +7,8 @@ window.FRY = { authorUrl: "https://github.com/Nexoniarz", repo: "https://github.com/Nexoniarz/Fry3DX", tagline: "A 3D modeller that does what you'd expect.", - intro: "Pick a tool, click, drag. Plain words instead of jargon, a toolbox like GIMP's, and the useful stuff from the big modellers without the hundred menus around it.", + intro: "Pick a tool, click, drag. Plain words instead of jargon, a toolbox like GIMP's, and the useful stuff from the big modellers without the hundred menus around it. On Windows, Linux and your phone.", + releases: "https://github.com/Nexoniarz/Fry3DX/releases/latest", why: { title: "Why does this exist?", @@ -29,6 +30,7 @@ window.FRY = { { icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." }, { icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." }, { icon: "🗀", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." }, + { icon: "📱", title: "On your phone too", text: "Not a viewer: the whole modeller. Big buttons, one finger turns the view, two move and pinch to zoom, holding still is a right-click. Files through the phone's own picker." }, { icon: "💾", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." }, ], @@ -36,6 +38,7 @@ window.FRY = { { img: "img/model.webp", title: "Model", text: "Starter models, a folder of parts, a shape drawn onto a cube's face, rendered with sky and shadows." }, { img: "img/paint.webp", title: "Paint", text: "A picture pasted on as a sticker: it bends over the cube's edge like a real one." }, { img: "img/print.webp", title: "Print", text: "The printer's plate, sizes in millimetres, checks and overhangs before you slice." }, + { img: "img/phone.webp", title: "Phone", text: "The phone layout: big buttons along the bottom, panels when you want them, fingers for the view." }, { img: "img/combine.webp", title: "Combine", text: "A cylinder taken out of a cube, copies in a row in a folder, a plane made thick." }, { img: "img/crash.webp", title: "Even the crash screen", text: "If something does get burnt, your work comes back from the last autosave." }, ], @@ -58,18 +61,25 @@ window.FRY = { ["F1", "Every key, in the app"], ], + downloads: [ + { os: "Windows", what: "10 and 11, 64-bit", file: "windows-x64.zip", how: "Unpack it, run fry3dx.exe." }, + { os: "Linux", what: "any distro from 2022 on, 64-bit", file: "linux-x64.AppImage", how: "Make it executable, run it. A .tar.gz is there too." }, + { os: "Android", what: "8.0 and newer, 64-bit, Vulkan", file: "android-arm64.apk", how: "Open it on the phone to install (allow installing from your browser or files app)." }, + ], + build: { - text: "Fry3DX builds from source on Linux with CMake. Everything else it needs is downloaded while building. On NixOS, shell.nix has the tools.", + text: "Or build it from source with CMake: everything else it needs is downloaded while building. On NixOS, shell.nix has the tools.", steps: [ "git clone https://github.com/Nexoniarz/Fry3DX.git", "cd Fry3DX", - "nix-shell --run 'cmake --preset release && cmake --build build/release'", + "cmake --preset release && cmake --build build/release", "build/release/bin/fry3dx", ], - needs: "Needs: a Vulkan graphics card, CMake, Ninja, a C++20 compiler, glslc (shaderc) and SDL3.", + needs: "Needs: CMake 3.25+, Ninja, a C++20 compiler (GCC 13+, Clang 17+) and glslc. Presets for Windows (cross-built) and Android are in the README.", }, versions: [ + { v: "0.5", date: "2026-10-19", items: ["Builds for Windows, Linux (AppImage) and Android", "A phone layout: big buttons, finger gestures, the phone's file picker", "Apache License 2.0"] }, { v: "0.4", date: "2026-10-19", items: ["Combine objects: add, subtract, overlap", "Draw shapes onto other objects", "Copies in a row or circle, thicken and hollow", "FBX export", "Folders and groups in Layers, go-to buttons", "Shift scales both sides, Shift+click loops and rings", "A crash screen of its own"] }, { v: "0.3", date: "2026-10-19", items: ["Stickers that bend over edges, picture layers", "Packed .fry files, autosave and recovery", "Layout tab, Print tab, 3MF / STL / OBJ / PLY / GLB export", "Hole tool: hexagon nut pockets, rectangles, outlines"] }, { v: "0.1", date: "2026-10-19", items: ["First version: shapes, shape editing, knife, painting, rendering"] }, diff --git a/docs/img/phone.webp b/docs/img/phone.webp new file mode 100644 index 0000000000000000000000000000000000000000..178a897fcfec847e3053f5caa777c18936f6b9d4 GIT binary patch literal 43742 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

Get it

+

@@ -77,7 +78,7 @@

Fry3DX, made by .

-

Sky pictures from Poly Haven (CC0).

+

Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0).

diff --git a/docs/site.js b/docs/site.js index 9952e8a..1f68522 100644 --- a/docs/site.js +++ b/docs/site.js @@ -65,6 +65,14 @@ keys.append(tr); } + const dl = $("downloads"); + for (const d of F.downloads) { + const a = el("a", "download"); + a.href = F.releases; + a.append(el("strong", null, d.os), el("span", "muted", d.what), el("span", "file", "Fry3DX-…-" + d.file), el("span", null, d.how)); + dl.append(a); + } + $("build-text").textContent = F.build.text; $("build-steps").textContent = F.build.steps.join("\n"); $("build-needs").textContent = F.build.needs; diff --git a/docs/style.css b/docs/style.css index 6799fc1..89e83dc 100644 --- a/docs/style.css +++ b/docs/style.css @@ -82,6 +82,11 @@ section:last-child { border-bottom: 0; } .keys td:first-child { width: 45%; } kbd { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; } +.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 28px; } +.download { display: flex; flex-direction: column; gap: 4px; padding: 18px 20px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); color: var(--ink); } +.download:hover { text-decoration: none; border-color: var(--hot); } +.download strong { font-size: 1.15rem; color: var(--hot); } +.download .file { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; color: #cfcdc7; } .code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); } .code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 ui-monospace, "DejaVu Sans Mono", monospace; color: #d6d4cd; } .copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; } diff --git a/engine/engine.cpp b/engine/engine.cpp index 5442c38..779a7fb 100644 --- a/engine/engine.cpp +++ b/engine/engine.cpp @@ -312,13 +312,7 @@ int runClient(Game& game) { window.setIcon(px, w, h); stbi_image_free(px); } - } else if (auto data = fs::read("branding/keel_emblem.yapf")) - if (yapf_texture_t* tex = yapf_tex_load_memory(data->data(), data->size(), 0, 1, nullptr)) { - std::vector rgba(size_t(tex->width) * tex->height * 4); - if (yapf_tex_to_rgba8(tex, yapf_tex_surface(tex, 0, 0, 0, 0), 0, rgba.data()) == YAPF_OK) - window.setIcon(rgba.data(), int(tex->width), int(tex->height)); - yapf_tex_free(tex); - } + } if (!g_testMode) audio::init(); ui::init(); log::info("Press ` (tilde key) to open the console"); @@ -539,17 +533,16 @@ int run(Game& game, int argc, char** argv) { crash::install(g_prefPath, args, dog); } - // Content: the engine's own (/keel/), then the game's (//) over it, + // Content: the engine's own (/engine/), then the game's (//) over it, // each with its packs — like Source's hl2/ under a mod. #if defined(__ANDROID__) const std::string exeDir = unpackAndroidContent(); - if (const char* cache = SDL_GetAndroidCachePath()) setenv("TMPDIR", cache, 1); // temp files (script modules) go there - const bool firstRun = !std::filesystem::exists(g_configPath); + if (const char* cache = SDL_GetAndroidCachePath()) setenv("TMPDIR", cache, 1); // temp files go there #else const char* base = SDL_GetBasePath(); const std::string exeDir = base ? base : "./"; #endif - if (!fs::mountGameDir(exeDir + "keel")) log::warn("Engine folder {}keel not found; fonts and UI won't load", exeDir); + if (!fs::mountGameDir(exeDir + "engine")) log::warn("Engine folder {}engine not found; fonts and UI won't load", exeDir); const std::string gameDir = exeDir + game.name(); if (!fs::mountGameDir(gameDir)) log::warn("Game folder {} not found; no content will load", gameDir); @@ -558,9 +551,6 @@ int run(Game& game, int argc, char** argv) { // Like Source's config_default.cfg: default binds/settings, then the player's own. if (!g_dedicated && fs::exists("cfg/default.cfg")) execute("exec default.cfg"); if (std::filesystem::exists(g_configPath)) execute("exec \"" + g_configPath + "\""); -#if defined(__ANDROID__) - if (firstRun) execute("preset Low"); // a phone: start light (the options change it) -#endif if (fs::exists("cfg/autoexec.cfg")) execute("exec autoexec.cfg"); executeCommandLine(startup, 0); g_modsDir = exeDir + "mods/" + game.name(); diff --git a/engine/rhi/gl/gl_backend.cpp b/engine/rhi/gl/gl_backend.cpp index 0814840..e3169fb 100644 --- a/engine/rhi/gl/gl_backend.cpp +++ b/engine/rhi/gl/gl_backend.cpp @@ -9,6 +9,7 @@ // window-space winding, so front faces are clockwise. Only the final blit to the window // (which shows row 0 at the bottom) flips. // Requires ARB_bindless_texture (NVIDIA, AMD); without it init fails and the engine uses Vulkan. +#include "ui/ui.h" #include "render/backend.h" #include "render/screenshot.h" #include "rhi/gl/gl_loader.h" @@ -332,6 +333,7 @@ public: void beginUi() override { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL3_NewFrame(); + ui::applyPixelScale(); ImGui::NewFrame(); } void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); } diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index adf0d11..90251b5 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -6,6 +6,7 @@ // 3.1 lightmaps, sun with shadows (one tap), sky, fog; flat surfaces, no highlights or lights // Neither has clustered lights, light shadows or occlusion culling. Depth is GL's -1..1 here // (no glClipControl), so vertex shaders turn the 0..1 matrices' z into it. +#include "ui/ui.h" #include "render/backend.h" #include "core/log.h" #include "platform/window.h" @@ -480,6 +481,7 @@ public: void beginUi() override { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL3_NewFrame(); + ui::applyPixelScale(); ImGui::NewFrame(); } void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); } diff --git a/engine/rhi/vulkan/vk_backend.cpp b/engine/rhi/vulkan/vk_backend.cpp index 18a5280..fb64939 100644 --- a/engine/rhi/vulkan/vk_backend.cpp +++ b/engine/rhi/vulkan/vk_backend.cpp @@ -1,5 +1,6 @@ // The Vulkan 1.3 backend: bindless textures (one descriptor array), buffer device addresses // for every per-draw and per-frame buffer (passed in push constants), dynamic rendering. +#include "ui/ui.h" #include "render/backend.h" #include "render/screenshot.h" #include "rhi/vulkan/vk_device.h" @@ -398,6 +399,7 @@ public: void beginUi() override { ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL3_NewFrame(); + ui::applyPixelScale(); ImGui::NewFrame(); } void setVsync(bool on) override { device_.setVsync(on); } diff --git a/engine/ui/ui.cpp b/engine/ui/ui.cpp index 387c2d3..3489d8c 100644 --- a/engine/ui/ui.cpp +++ b/engine/ui/ui.cpp @@ -3,6 +3,7 @@ #include "fs/vfs.h" #include +#include #include #include @@ -95,6 +96,29 @@ ImFont* font(Font f) { float scale() { return g_scale; } +namespace { +float g_pixelScale = 1.0f; +ImU32 g_lastScaledEvent = 0; +} +void setPixelScale(float k) { g_pixelScale = std::clamp(k, 0.5f, 6.0f); } +float pixelScale() { return g_pixelScale; } +void applyPixelScale() { + const float k = g_pixelScale; + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x /= k, io.DisplaySize.y /= k; // (set again each frame by the platform) + io.DisplayFramebufferScale.x *= k, io.DisplayFramebufferScale.y *= k; + // The mouse's positions queued since last frame, in the screen's pixels: into the UI's. + // (Events left over for the next frame were done already: by their number.) + ImU32 last = g_lastScaledEvent; + for (ImGuiInputEvent& e : GImGui->InputEventsQueue) { + if (e.EventId <= g_lastScaledEvent) continue; + last = std::max(last, e.EventId); + if (e.Type == ImGuiInputEventType_MousePos && e.MousePos.PosX > -FLT_MAX) + e.MousePos.PosX /= k, e.MousePos.PosY /= k; + } + g_lastScaledEvent = last; +} + void beginFrame(float windowHeight) { g_scale = std::clamp(windowHeight / 1080.0f, 0.75f, 2.5f); ImGui::GetStyle().FontScaleMain = g_scale; diff --git a/engine/ui/ui.h b/engine/ui/ui.h index bff8636..3130928 100644 --- a/engine/ui/ui.h +++ b/engine/ui/ui.h @@ -18,6 +18,12 @@ ImFont* font(Font f); // Sizes in pixels; scaled with the window height so 1080p looks like Source at 1080p. float scale(); void beginFrame(float windowHeight); +// The UI laid out `k` times smaller than the screen's pixels and drawn scaled up, everything +// alike (a phone: its pixels are tiny). 1: as it is. +void setPixelScale(float k); +float pixelScale(); +// The backends call this between the platform's NewFrame and ImGui::NewFrame. +void applyPixelScale(); // The highlight colour: highlights, the logo. inline constexpr uint8_t kAccent[3] = {227, 34, 45}; diff --git a/shell.nix b/shell.nix index d565327..6aacdac 100644 --- a/shell.nix +++ b/shell.nix @@ -1,4 +1,6 @@ # Dev shell for NixOS: `nix-shell`, then `cmake --preset debug && cmake --build build/debug`. +# Windows builds too (cmake --preset windows, see cmake/toolchains/windows-msvc.cmake); +# Android: android/shell.nix. { pkgs ? import {} }: pkgs.mkShell { packages = with pkgs; [ @@ -7,7 +9,10 @@ pkgs.mkShell { shaderc # glslc sdl3 vulkan-loader vulkan-validation-layers vulkan-tools + llvmPackages_19.clang-unwrapped llvmPackages_19.lld llvmPackages_19.llvm xwin ]; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [ vulkan-loader sdl3 ]); VK_LAYER_PATH = "${pkgs.vulkan-validation-layers}/share/vulkan/explicit_layer.d"; + # clang's own headers for the Windows cross compiler (Nix keeps them outside its resource dir). + KEEL_CLANG_INCLUDE = "${pkgs.llvmPackages_19.libclang.lib}/lib/clang/19/include"; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 81959ff..c0708d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,9 +5,23 @@ target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(fry_core PUBLIC glm::glm libzstd_static) target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL) -add_executable(fry3dx app.cpp icons.cpp) +if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main + add_library(fry3dx SHARED app.cpp icons.cpp) + set_target_properties(fry3dx PROPERTIES OUTPUT_NAME main LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +else() + add_executable(fry3dx app.cpp icons.cpp) +endif() +if(WIN32) # a window program (no console window), with its icon + target_sources(fry3dx PRIVATE fry3dx.rc) + set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON) + target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup) +endif() target_link_libraries(fry3dx PRIVATE fry_core keel stb) add_dependencies(fry3dx content) foreach(t fry_core fry3dx) - target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-missing-field-initializers) + if(MSVC) + target_compile_options(${t} PRIVATE /W3) + else() + target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-missing-field-initializers) + endif() endforeach() diff --git a/src/app.cpp b/src/app.cpp index 839e743..cca894e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -9,12 +9,16 @@ #include "shapes.h" #include +#if defined(__ANDROID__) +#include // (main becomes the SDL_main SDL's Java side calls) +#endif #include #include #include #include #include #include +#include #include #include @@ -218,6 +222,13 @@ keel::Cvar fry_printer("fry_printer", "0", keel::CVAR_ARCHIVE, "Fry3DX: the 3D p keel::Cvar fry_bed_x("fry_bed_x", "220", keel::CVAR_ARCHIVE, "Fry3DX: your own printer's build plate across (mm)"); keel::Cvar fry_bed_y("fry_bed_y", "220", keel::CVAR_ARCHIVE, "Fry3DX: your own printer's build plate deep (mm)"); keel::Cvar fry_bed_z("fry_bed_z", "250", keel::CVAR_ARCHIVE, "Fry3DX: how high your own printer prints (mm)"); +#if defined(__ANDROID__) +keel::Cvar fry_touch("fry_touch", "1", keel::CVAR_ARCHIVE, "Fry3DX: the phone layout and touch (takes effect on the next start)"); +#else +keel::Cvar fry_touch("fry_touch", "0", keel::CVAR_ARCHIVE, "Fry3DX: the phone layout and touch (takes effect on the next start)"); +#endif +keel::Cvar fry_uiscale("fry_uiscale", "0", 0, "Fry3DX: draw the UI this many times bigger (0: by the screen)"); +keel::Cvar fry_uisize("fry_uisize", "1", keel::CVAR_ARCHIVE, "Fry3DX: the phone layout's size, times"); keel::Cvar fry_autosave("fry_autosave", "2", keel::CVAR_ARCHIVE, "Fry3DX: autosave every this many minutes while there are changes (0: off)"); class App : public keel::Game { @@ -341,6 +352,7 @@ public: self->groupSelected(); if (a.size() > 1 && !self->scene_.folders.empty()) self->scene_.folders.back().name = a[1], self->renamingFolder_ = 0; }, "Fry3DX: fry_group [name]: the picked into a new folder"); + static keel::Cmd panelsCmd("fry_panels", [](const keel::Args&) { self->panelsOpen_ = !self->panelsOpen_; }, "Fry3DX: phone layout: show / hide the panels"); static keel::Cmd goCmd("fry_goto", [](const keel::Args&) { self->goTo(self->selected_); }, "Fry3DX: the camera to the picked"); static keel::Cmd drawCmd("fry_draw", [](const keel::Args& a) { // fry_draw mode thick x y x y x y ... (view 0..1) if (a.size() < 9) return; @@ -497,6 +509,7 @@ public: applyShading(); takeDialogResult(); ImGuiIO& io = ImGui::GetIO(); + if (touch()) touchFrame(io); const ImGuiID dock = ImGui::DockSpaceOverViewport(0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode); if (!layoutDone_) defaultLayout(dock); if (ImGuiDockNode* c = ImGui::DockBuilderGetCentralNode(dock)) viewMin_ = c->Pos, viewMax_ = ImVec2(c->Pos.x + c->Size.x, c->Pos.y + c->Size.y); @@ -504,11 +517,14 @@ public: menuBar(); toolbox(); toolOptions(); - layersPanel(); - propertiesPanel(); - if (tab_ == Tab::Paint || tab_ == Tab::Layout) picturePanel(); - statusBar(); - if (!capturing()) viewButtons(); + if (!touch() || panelsOpen_) { + layersPanel(); + propertiesPanel(); + if (tab_ == Tab::Paint || tab_ == Tab::Layout) picturePanel(); + } + if (!touch()) statusBar(); + if (!capturing() && (!touch() || viewsOpen_)) viewButtons(); + if (!capturing() && touch()) touchBar(); if (showHelp_) helpWindow(); if (showAbout_) aboutWindow(); copiesWindow(); @@ -644,6 +660,15 @@ private: } ledit_; bool pasteOnSurface_ = true; uint32_t renamingFolder_ = 0; + // Touch (set when it starts: fry_touch). + bool touchMode_ = false, panelsOpen_ = false, viewsOpen_ = false, shiftLatch_ = false, ctrlLatch_ = false, touchOrbit_ = false; + struct Gesture { + bool two = false, longDone = false, rightUp = false; + ImVec2 mid{}, downPos{}; + float spread = 1; + double downAt = -1; + } gest_; + std::string captureUri_; // (a picture of the view going into a picked file) struct CopiesSetting { int count = 3, axis = 2; bool round = false, joined = false; @@ -899,8 +924,46 @@ private: // ───────────────────────────── files ───────────────────────────── - void openFile(const std::string& path) { + // ── Files a phone's file picker gives (content://...): read and written through SDL ── + static bool isUri(const std::string& p) { return p.starts_with("content://"); } + // A file's name as shown (a picked file's: from the end of its address, decoded). + static std::string fileName(const std::string& p) { + if (!isUri(p)) return stdfs::path(p).filename().string(); + std::string d; + for (size_t i = 0; i < p.size(); ++i) + if (p[i] == '%' && i + 2 < p.size()) d += char(std::stoi(p.substr(i + 1, 2), nullptr, 16)), i += 2; + else d += p[i]; + return d.substr(d.find_last_of("/:") + 1); + } + // A picked file copied to a file of our own to read ("" if it couldn't be). + static std::string fromUri(const std::string& uri, const std::string& name) { + size_t n = 0; + void* data = SDL_LoadFile(uri.c_str(), &n); + if (!data) return ""; + std::error_code ec; + const std::string local = (stdfs::temp_directory_path(ec) / name).string(); + std::ofstream(local, std::ios::binary).write(static_cast(data), std::streamsize(n)); + SDL_free(data); + return local; + } + // A file of ours written into the picked one. + static bool toUri(const std::string& local, const std::string& uri) { + std::ifstream in(local, std::ios::binary); + const std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + SDL_IOStream* io = SDL_IOFromFile(uri.c_str(), "wb"); + if (!io) return false; + const bool ok = SDL_WriteIO(io, bytes.data(), bytes.size()) == bytes.size(); + return SDL_CloseIO(io) && ok; + } + static std::string tempFile(const std::string& name) { + std::error_code ec; + return (stdfs::temp_directory_path(ec) / name).string(); + } + + void openFile(const std::string& picked) { std::string err; + const std::string path = isUri(picked) ? fromUri(picked, "opened.fry") : picked; + if (path.empty()) return say("Couldn't read that file"); auto s = loadPackage(path, &err); if (!s) return say("Couldn't open: " + err); dropAutosave(); @@ -919,21 +982,23 @@ private: } std::erase_if(o.layers, [](const PictureLayer& l) { return !l.image; }); } - path_ = path; + path_ = picked; undo_.clear(), redo_.clear(), selected_.clear(); mode_ = Mode::Object, active_ = 0, dirty_ = false, ledit_ = {}; touchAll(); frameAll(); - say("Opened " + stdfs::path(path).filename().string()); + say("Opened " + fileName(picked)); } bool saveFile(std::string path) { if (path.empty()) return false; - if (stdfs::path(path).extension() != ".fry") path += ".fry"; + if (!isUri(path) && stdfs::path(path).extension() != ".fry") path += ".fry"; + const std::string local = isUri(path) ? tempFile("saving.fry") : path; std::string err; - if (!savePackage(path, scene_, 12, &err)) return afterSave_ = 0, say("Couldn't save: " + err), false; + if (!savePackage(local, scene_, 12, &err)) return afterSave_ = 0, say("Couldn't save: " + err), false; + if (isUri(path) && !toUri(local, path)) return afterSave_ = 0, say("Couldn't write the file you picked"), false; dropAutosave(); path_ = path, dirty_ = false; - say(std::format("Saved {} ({})", stdfs::path(path).filename().string(), sizeText(stdfs::file_size(path)))); + say(std::format("Saved {} ({})", fileName(path), sizeText(stdfs::file_size(local)))); if (const int then = std::exchange(afterSave_, 0)) doAfterAsking(then); return true; } @@ -946,8 +1011,16 @@ private: &out, im.w, im.h, 4, im.px.data(), im.w * 4); return out; } - void exportFile(std::string path, int filter = -1) { + void exportFile(std::string path, int filter = -1, const std::string& asExt = "") { if (path.empty()) return; + if (isUri(path)) { // (a picked file: made here, then written into it; one file only) + const auto& formats = exportFormats(); + const std::string ext = !asExt.empty() ? asExt : formats[size_t(filter >= 0 && filter < int(formats.size()) ? filter : 0)].first; + const std::string local = tempFile("export" + ext); + exportFile(local); + if (!toUri(local, path)) say("Couldn't write the file you picked"); + return; + } std::string ext = stdfs::path(path).extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return char(std::tolower(c)); }); const auto& formats = exportFormats(); @@ -979,7 +1052,7 @@ private: const ImVec2 c = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(c, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (!ImGui::BeginPopupModal("Save changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) return; - const std::string name = path_.empty() ? "Untitled" : stdfs::path(path_).filename().string(); + const std::string name = path_.empty() ? "Untitled" : fileName(path_); ImGui::Text("%s has changes that aren't saved.", name.c_str()); ImGui::TextDisabled(askSave_ == 1 ? "Save them before closing?" : askSave_ == 2 ? "Save them before starting a new one?" : "Save them before opening another?"); ImGui::Spacing(); @@ -1051,9 +1124,18 @@ private: std::string from; std::getline(in, from); std::error_code ec; - const auto when = std::chrono::clock_cast(stdfs::last_write_time(f, ec)); - const std::string label = std::format("{} {:%Y-%m-%d %H:%M} {}", from.empty() ? "Untitled" : stdfs::path(from).filename().string(), - std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::floor(when)), sizeText(stdfs::file_size(f, ec))); + // (in local time: through time_t, as not every C++ library has time zones yet) + const std::time_t t = std::chrono::system_clock::to_time_t( + std::chrono::time_point_cast(stdfs::file_time_type::clock::to_sys(stdfs::last_write_time(f, ec)))); + std::tm local{}; +#if defined(_WIN32) + localtime_s(&local, &t); +#else + localtime_r(&t, &local); +#endif + char when[32]; + std::strftime(when, sizeof when, "%Y-%m-%d %H:%M", &local); + const std::string label = std::format("{} {} {}", from.empty() ? "Untitled" : stdfs::path(from).filename().string(), when, sizeText(stdfs::file_size(f, ec))); ImGui::PushID(f.c_str()); if (ImGui::Button("Open", ImVec2(70, 0))) open = f; ImGui::SameLine(); @@ -1105,6 +1187,8 @@ private: stbi_image_free(px); std::error_code ec; stdfs::remove(tmp, ec); + if (const std::string uri = std::exchange(captureUri_, {}); ok && !uri.empty()) + return say(toUri(capturePath_, uri) ? "Saved a picture of the view: " + fileName(uri) : "Couldn't write the file you picked"); say(ok ? "Saved a picture of the view: " + stdfs::path(capturePath_).filename().string() : "Couldn't write " + capturePath_); } static bool loadImageFile(const std::string& path, Image& out) { @@ -1146,10 +1230,10 @@ private: else self->dialogCancelled_ = true; }; SDL_Window* w = SDL_GetKeyboardFocus(); - const std::string here = path_.empty() ? std::string() : stdfs::path(path_).parent_path().string(); + const std::string here = path_.empty() || isUri(path_) ? std::string() : stdfs::path(path_).parent_path().string(); const char* at = here.empty() ? nullptr : here.c_str(); if (kind == 1) SDL_ShowOpenFileDialog(done, this, w, fry, 1, at, false); - else if (kind == 2) SDL_ShowSaveFileDialog(done, this, w, fry, 1, path_.empty() ? at : path_.c_str()); + else if (kind == 2) SDL_ShowSaveFileDialog(done, this, w, fry, 1, path_.empty() || isUri(path_) ? at : path_.c_str()); else if (kind == 3) SDL_ShowSaveFileDialog(done, this, w, models.data(), int(models.size()), at); else if (kind == 8 || kind == 9) { static const SDL_DialogFileFilter mf[] = {{"3MF for slicers", "3mf"}}, stl[] = {{"STL", "stl"}}; @@ -1167,6 +1251,16 @@ private: if (std::exchange(dialogCancelled_, false)) afterSave_ = 0; // (Save, then quit: but the saving was cancelled) } if (file.empty()) return; + if (isUri(file) && (kind == 4 || kind == 6)) file = fromUri(file, std::format("picked-{}.img", SDL_GetTicks())); // (pictures: read from a copy) + if (isUri(file) && (kind == 5 || kind == 7)) { // (pictures saved: made here, then written into the picked file) + const std::string uri = file; + file = tempFile("picture.png"); + if (kind == 5) return savePicture(file), void(captureUri_ = uri); // (written into it once it's made) + if (Object* o = find(active_); o && o->picture) writePng(file, composePicture(*o)); + say(toUri(file, uri) ? "Saved " + fileName(uri) : "Couldn't write the file you picked"); + return; + } + if (isUri(file) && (kind == 8 || kind == 9)) return exportFile(file, -1, kind == 8 ? ".3mf" : ".stl"); if (kind == 1) openFile(file); else if (kind == 2) saveFile(file); else if (kind == 3) exportFile(file, filter); @@ -3115,6 +3209,109 @@ private: say(n ? std::format("{} faces joined into the flat ones beside them", n) : "Nothing to join: no flat faces side by side"); } + // ───────────────────────────── touch: phones and tablets ───────────────────────────── + // A phone: everything drawn bigger, the side panels only when asked for, a bar of big buttons + // at the bottom (undo, Shift and Ctrl that stay down, editing the shape...), and fingers: + // one drags the view round (or a tool's arrows), two move it and pinch to zoom, holding one + // still is a right-click. + + bool touch() const { return touchMode_; } + // How much bigger than its pixels the UI is drawn. + float touchScale() const { + if (fry_uiscale.f() > 0) return fry_uiscale.f(); + if (!touch()) return 1.0f; + const float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); + return std::clamp((density > 0 ? density : 2.0f) * 0.9f, 1.0f, 4.0f) * std::clamp(fry_uisize.f(), 0.5f, 2.0f); + } + void touchFrame(ImGuiIO& io) { + if (shiftLatch_) io.KeyShift = true; + if (ctrlLatch_) io.KeyCtrl = true; + ImGui::GetStyle().FontScaleMain = 1.0f; // (the sizes come from touchScale) + if (gest_.rightUp) { // (the long press's right-click: let go the frame after) + io.AddMouseButtonEvent(ImGuiMouseButton_Right, false); + gest_.rightUp = false; + } + // The fingers on the screen now (where, in the UI's units). + std::vector fingers; + int devices = 0; + if (SDL_TouchID* ids = SDL_GetTouchDevices(&devices)) { + for (int d = 0; d < devices; ++d) { + int n = 0; + if (SDL_Finger** f = SDL_GetTouchFingers(ids[d], &n)) { + for (int i = 0; i < n; ++i) fingers.push_back(ImVec2(f[i]->x * io.DisplaySize.x, f[i]->y * io.DisplaySize.y)); + SDL_free(f); + } + } + SDL_free(ids); + } + auto inView = [&](ImVec2 p) { return p.x >= viewMin_.x && p.y >= viewMin_.y && p.x < viewMax_.x && p.y < viewMax_.y; }; + if (fingers.size() >= 2) { + const ImVec2 mid((fingers[0].x + fingers[1].x) * 0.5f, (fingers[0].y + fingers[1].y) * 0.5f); + const float spread = std::max(std::hypot(fingers[0].x - fingers[1].x, fingers[0].y - fingers[1].y), 1.0f); + if (!gest_.two && !drag_.active && inView(mid) && !ImGui::IsAnyItemActive()) { // two fingers: the first's tap or drag is off + gest_.two = true, gest_.mid = mid, gest_.spread = spread; + boxing_ = lassoing_ = touchOrbit_ = false; + io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); + } + if (gest_.two) { + panBy(ImVec2(mid.x - gest_.mid.x, mid.y - gest_.mid.y)); + dist_ = std::clamp(dist_ * gest_.spread / spread, 0.01f, 5000.0f); + gest_.mid = mid, gest_.spread = spread; + } + gest_.downAt = -1; + return; + } + if (fingers.empty()) gest_.two = false; + if (fingers.size() != 1 || gest_.two) return void(gest_.downAt = -1); + // One finger held still: a right-click there (menus in the panels, orbit in the view). + const ImVec2 p = fingers[0]; + if (gest_.downAt < 0) gest_.downAt = now(), gest_.downPos = p, gest_.longDone = false; + if (std::hypot(p.x - gest_.downPos.x, p.y - gest_.downPos.y) > 12) gest_.downAt = now() + 1e9; // (moved: not a long press) + if (!gest_.longDone && now() - gest_.downAt > 0.55 && !drag_.active) { + gest_.longDone = true; + boxing_ = lassoing_ = touchOrbit_ = false; + io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); + io.AddMouseButtonEvent(ImGuiMouseButton_Right, true); + gest_.rightUp = true; + } + } + // The bar along the bottom of the view: big buttons, in rows if the view's narrow. + void touchBar() { + const float maxW = std::max(viewMax_.x - viewMin_.x - 16, 120.0f); + ImGui::SetNextWindowPos(ImVec2((viewMin_.x + viewMax_.x) * 0.5f, viewMax_.y - 6), ImGuiCond_Always, ImVec2(0.5f, 1.0f)); + ImGui::SetNextWindowBgAlpha(0.8f); + ImGui::Begin("##touchbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking); + if (now() - messageAt_ < 4.0) ImGui::PushTextWrapPos(maxW), ImGui::TextColored(kAccentText, "%s", message_.c_str()), ImGui::PopTextWrapPos(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12, 8)); + const ImGuiStyle& st = ImGui::GetStyle(); + float x = 0; // (where the row's got to) + auto button = [&](const char* label, bool on = false, const char* tip = nullptr) { + const float w = ImGui::CalcTextSize(label).x + st.FramePadding.x * 2; + if (x > 0 && x + st.ItemSpacing.x + w <= maxW) ImGui::SameLine(), x += st.ItemSpacing.x; + else x = 0; + x += w; + if (on) ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + const bool pressed = ImGui::Button(label); + if (on) ImGui::PopStyleColor(); + if (tip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip); + return pressed; + }; + if (button("Undo")) undo(); + if (button("Redo")) redo(); + if (button("Shift", shiftLatch_, "Held down until tapped again: adds to the selection, picks loops, scales both sides")) shiftLatch_ = !shiftLatch_; + if (button("Ctrl", ctrlLatch_, "Held down until tapped again: takes from the selection, snaps")) ctrlLatch_ = !ctrlLatch_; + if (tab_ == Tab::Model) { + if (button(mode_ == Mode::Edit ? "Done" : "Edit shape", mode_ == Mode::Edit)) mode_ == Mode::Edit ? leaveEdit() : enterEdit(); + if (button("Delete")) deleteSelected(); + } + if (button("Frame")) frameSelected(); + if (button("Views", viewsOpen_)) viewsOpen_ = !viewsOpen_; + if (button("Panels", panelsOpen_, "Layers, Properties (and the Picture when painting)")) panelsOpen_ = !panelsOpen_; + ImGui::PopStyleVar(); + ImGui::End(); + } + // ───────────────────────────── the camera ───────────────────────────── glm::vec3 viewDir() const { return forwardOf(yaw_, pitch_); } @@ -3922,8 +4119,11 @@ private: return; } if (boxing_) { + const bool moved = std::hypot(io.MousePos.x - boxStart_.x, io.MousePos.y - boxStart_.y) > 6; + if (touchOrbit_ && moved && ImGui::IsMouseDown(ImGuiMouseButton_Left)) orbitBy(io.MouseDelta); if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { boxing_ = false; + if (std::exchange(touchOrbit_, false) && moved) return; if (std::hypot(io.MousePos.x - boxStart_.x, io.MousePos.y - boxStart_.y) > 4) { const ImVec2 a(std::min(boxStart_.x, io.MousePos.x), std::min(boxStart_.y, io.MousePos.y)), b(std::max(boxStart_.x, io.MousePos.x), std::max(boxStart_.y, io.MousePos.y)); regionSelect([&](ImVec2 s) { return s.x >= a.x && s.x <= b.x && s.y >= a.y && s.y <= b.y; }, io.KeyShift, io.KeyCtrl, !io.KeyShift && !io.KeyCtrl); @@ -4004,7 +4204,7 @@ private: if (!io.KeyShift && !io.KeyCtrl) selectNone(); paint(io); } else if (selShape_ == SelShape::Lasso) lassoing_ = true, lasso_ = {io.MousePos}; - else boxing_ = true, boxStart_ = io.MousePos; + else boxing_ = true, boxStart_ = io.MousePos, touchOrbit_ = touch() && tool_ != Tool::Select; // (a finger: dragging turns the view, but Select's box) } } // Painting: on the model under the mouse. @@ -4644,8 +4844,10 @@ private: // ───────────────────────────── panels ───────────────────────────── void setupUi() { + keel::ui::setPixelScale(touchScale()); if (iniPath_.empty()) { - iniPath_ = keel::prefPath() + "layout_v3.ini"; + touchMode_ = fry_touch.b(); + iniPath_ = keel::prefPath() + (touchMode_ ? "layout_phone.ini" : "layout_v3.ini"); ImGui::GetIO().IniFilename = iniPath_.c_str(); if (stdfs::exists(iniPath_)) ImGui::LoadIniSettingsFromDisk(iniPath_.c_str()); } @@ -4689,6 +4891,17 @@ private: if (node && (node->IsSplitNode() || node->Windows.Size)) return; // (the user's own, from the .ini) ImGui::DockBuilderRemoveNodeChildNodes(dock); ImGuiID center = dock; + if (touch()) { // a phone: tools down the left, the panels on the right (shown when asked for) + ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, ¢er); + const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.45f, nullptr, &left); + const ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.36f, nullptr, ¢er); + ImGui::DockBuilderDockWindow("Toolbox", left); + ImGui::DockBuilderDockWindow("Tool options", leftBottom); + ImGui::DockBuilderDockWindow("Properties", right); + ImGui::DockBuilderDockWindow("Layers", right); + ImGui::DockBuilderDockWindow("Picture", right); + return ImGui::DockBuilderFinish(dock); + } ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.14f, nullptr, ¢er); const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.5f, nullptr, &left); ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.22f, nullptr, ¢er); @@ -4705,14 +4918,7 @@ private: ImGui::DockBuilderFinish(dock); } - void menuBar() { - if (!ImGui::BeginMainMenuBar()) return; - if (haveLogo_) { - const float h = ImGui::GetFrameHeight() - 4; - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1); - logoImage(h); - } - ImGui::TextColored(ImVec4(1.0f, 0.47f, 0.16f, 1.0f), "Fry3DX"); + void menus() { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("New", "Ctrl+N")) whenSaved(2); if (ImGui::MenuItem("Open...", "Ctrl+O")) whenSaved(3); @@ -4827,13 +5033,27 @@ private: if (ImGui::MenuItem("About Fry3DX")) showAbout_ = true; ImGui::EndMenu(); } + } + void menuBar() { + if (!ImGui::BeginMainMenuBar()) return; + if (haveLogo_) { + const float h = ImGui::GetFrameHeight() - 4; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1); + logoImage(h); + } + ImGui::TextColored(ImVec4(1.0f, 0.47f, 0.16f, 1.0f), "Fry3DX"); + if (!touch()) menus(); + else if (ImGui::BeginMenu("Menu")) { // (a phone: one menu, the others inside it) + menus(); + ImGui::EndMenu(); + } // The tabs: what you're doing. { const char* names[] = {"Model", "Paint", "Layout", "Render", "Print"}; const Icon icons[] = {Icon::Objects, Icon::PaintBrush, Icon::Layout, Icon::Render, Icon::Print}; const char* tips[] = {"Make the shape (Ctrl+1)", "Paint pictures onto it (Ctrl+2)", "Change how its picture is laid out: move, turn and size its frames (Ctrl+3)", "Light it and save pictures of it (Ctrl+4)", "Get it ready for a 3D printer: size, checks, the printer's plate (Ctrl+5)"}; - const float tw = 92.0f; + const float tw = touch() ? 72.0f : 92.0f; ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX() + 20.0f, (ImGui::GetWindowWidth() - tw * 5 - 16) * 0.5f)); for (int i = 0; i < 5; ++i) { if (i) ImGui::SameLine(0, 4); @@ -4842,7 +5062,8 @@ private: ImGui::PopID(); } } - const std::string file = (path_.empty() ? "Untitled" : stdfs::path(path_).filename().string()) + (dirty_ ? " *" : ""); + const std::string file = (path_.empty() ? "Untitled" : fileName(path_)) + (dirty_ ? " *" : ""); + if (touch()) return ImGui::EndMainMenuBar(); // (no room on a phone) ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(file.c_str()).x - 16); ImGui::TextDisabled("%s", file.c_str()); ImGui::EndMainMenuBar(); @@ -5845,7 +6066,7 @@ private: const char* title = "Fry3DX — Frying 3D Extreme"; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize(title).x) * 0.5f); ImGui::TextColored(ImVec4(1.0f, 0.47f, 0.16f, 1.0f), "%s", title); - ImGui::TextWrapped("A 3D modeller that does what you'd expect: pick a tool, click and drag. Made by Nexoniarz."); + ImGui::TextWrapped("A 3D modeller that does what you'd expect: pick a tool, click and drag. Made by Nexoniarz; open source (Apache License 2.0)."); ImGui::End(); } }; diff --git a/src/fry3dx.rc b/src/fry3dx.rc new file mode 100644 index 0000000..5ae697b --- /dev/null +++ b/src/fry3dx.rc @@ -0,0 +1,25 @@ +// Windows: the program's icon and details (Explorer's Properties). +1 ICON "../branding/fry3dx.ico" +1 VERSIONINFO +FILEVERSION 0,5,0,0 +PRODUCTVERSION 0,5,0,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Nexoniarz" + VALUE "FileDescription", "Fry3DX - Frying 3D Extreme" + VALUE "FileVersion", "0.5.0" + VALUE "InternalName", "fry3dx" + VALUE "LegalCopyright", "Copyright 2026 Nexoniarz, Apache License 2.0" + VALUE "OriginalFilename", "fry3dx.exe" + VALUE "ProductName", "Fry3DX" + VALUE "ProductVersion", "0.5.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/tools/package.py b/tools/package.py new file mode 100644 index 0000000..4284518 --- /dev/null +++ b/tools/package.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Packs builds into the files people download, in dist/: + + Fry3DX--linux-x64.tar.gz from build/portable (cmake --preset portable) + Fry3DX--linux-x64.AppImage the same, as one file (needs appimagetool: $APPIMAGETOOL or on PATH) + Fry3DX--windows-x64.zip from build/windows, or build/ given with --windows-bin + + python3 tools/package.py linux|appimage|windows [--bin DIR] + +Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/), LICENSE and a +README.txt. (The Android APK: android/build_apk.py.) +""" +import os +import shutil +import subprocess +import sys +import tarfile +import zipfile + +ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +DIST = os.path.join(ROOT, "dist") +README = """Fry3DX {version} - Frying 3D Extreme +===================================== + +A 3D modeller that does what you'd expect: pick a tool, click, drag. +Made by Nexoniarz - https://nexoniarz.github.io/Fry3DX/ + +Start: {start} +Needs a graphics card with Vulkan (or OpenGL 4.6). F1 in the app lists every key. +Your settings and autosaves: {prefs} + +Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC0. +""" + + +def version(): + with open(os.path.join(ROOT, "CMakeLists.txt")) as f: + for line in f: + if line.startswith("project(Fry3DX VERSION "): + return line.split()[2] + return "0" + + +def stage(bin_dir, exe, extra, start, prefs): + out = os.path.join(DIST, "stage", "Fry3DX") + shutil.rmtree(os.path.dirname(out), ignore_errors=True) + os.makedirs(out) + for name in [exe, *extra]: + shutil.copy2(os.path.join(bin_dir, name), out) + for d in ("engine", "Fry3DX"): + shutil.copytree(os.path.join(bin_dir, d), os.path.join(out, d)) + shutil.copy2(os.path.join(ROOT, "LICENSE"), out) + with open(os.path.join(out, "README.txt"), "w", newline="\r\n" if exe.endswith(".exe") else "\n") as f: + f.write(README.format(version=version(), start=start, prefs=prefs)) + return out + + +def linux(bin_dir): + out = stage(bin_dir, "fry3dx", [], "./fry3dx (or double-click it)", "~/.local/share/Nexoniarz/Fry3DX") + subprocess.run(["strip", os.path.join(out, "fry3dx")], check=True) + path = os.path.join(DIST, f"Fry3DX-{version()}-linux-x64.tar.gz") + with tarfile.open(path, "w:gz") as t: + t.add(out, "Fry3DX") + return path + + +def appimage(bin_dir): + tool = os.environ.get("APPIMAGETOOL") or shutil.which("appimagetool") + if not tool: + sys.exit("No appimagetool (set APPIMAGETOOL)") + out = stage(bin_dir, "fry3dx", [], "the AppImage itself", "~/.local/share/Nexoniarz/Fry3DX") + app = os.path.join(DIST, "stage", "Fry3DX.AppDir") + # The program and its content where it looks for them (next to it), in usr/bin. + shutil.copytree(out, os.path.join(app, "usr", "bin")) + subprocess.run(["strip", os.path.join(app, "usr", "bin", "fry3dx")], check=True) + os.symlink("usr/bin/fry3dx", os.path.join(app, "AppRun")) + shutil.copy2(os.path.join(ROOT, "branding", "logo.png"), os.path.join(app, "fry3dx.png")) + with open(os.path.join(app, "fry3dx.desktop"), "w") as f: + f.write("[Desktop Entry]\nType=Application\nName=Fry3DX\nComment=A 3D modeller that does what you'd expect\n" + "Exec=fry3dx %f\nIcon=fry3dx\nCategories=Graphics;3DGraphics;\nTerminal=false\n") + path = os.path.join(DIST, f"Fry3DX-{version()}-linux-x64.AppImage") + subprocess.run([tool, "--no-appstream", app, path], check=True, env={**os.environ, "ARCH": "x86_64"}) + return path + + +def windows(bin_dir): + out = stage(bin_dir, "fry3dx.exe", ["SDL3.dll"], "fry3dx.exe", "%APPDATA%\\Nexoniarz\\Fry3DX") + path = os.path.join(DIST, f"Fry3DX-{version()}-windows-x64.zip") + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: + for r, _, names in os.walk(out): + for n in names: + full = os.path.join(r, n) + z.write(full, os.path.join("Fry3DX", os.path.relpath(full, out))) + return path + + +def main(): + args = sys.argv[1:] + bin_dir = None + if "--bin" in args: + i = args.index("--bin") + bin_dir = os.path.abspath(args[i + 1]) + del args[i:i + 2] + os.makedirs(DIST, exist_ok=True) + for what in args: + default = {"linux": "build/portable/bin", "appimage": "build/portable/bin", "windows": "build/windows/bin"}[what] + path = {"linux": linux, "appimage": appimage, "windows": windows}[what](bin_dir or os.path.join(ROOT, default)) + print(f"{os.path.relpath(path, ROOT)}: {os.path.getsize(path) / 1048576:.1f} MB") + shutil.rmtree(os.path.join(DIST, "stage"), ignore_errors=True) + + +if __name__ == "__main__": + main()