Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Bug report
description: Something crashes, looks wrong or does not open
description: Something crashes, looks wrong or doesn't open
labels: [bug]
body:
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, what did you expect, and what happened instead?
description: What did you do, what did you expect, and what happened instead? Screenshots help.
validations:
required: true
- type: input
Expand All @@ -26,4 +26,4 @@ body:
id: file
attributes:
label: Model file
description: If a specific file is affected, attach it (zip it first) or link to it if you can share it.
description: If one file is affected, attach it (zip it first) or link to it, if you can share it.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
- name: JVM tests (Robolectric, real native engine)
run: |
sudo apt-get install -y -q libegl1 libgles2 > /dev/null
./gradlew :app:testDebugUnitTest --tests '*ViewerFlowTest*' -Pfsv.hostLibDir="$PWD/host-native/native/build-host"
./gradlew :app:testDebugUnitTest --tests '*ViewerFlowTest*' --tests '*EngineReleaseTest*' --tests '*UpdateCheckTest*' -Pfsv.hostLibDir="$PWD/host-native/native/build-host"
- name: Assemble debug APKs
run: ./gradlew assembleDebug
- uses: actions/upload-artifact@v4
Expand Down
69 changes: 62 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
TAG: ${{ needs.version.outputs.tag }}
TEST_BUILD: ${{ github.event.inputs.test_build == 'true' }}
# Public fingerprint of the Meshwise release certificate (CN=Meshwise, O=doesthings).
EXPECTED_CERT_SHA256: cbefeb0e767375be72d29325f97ec602b4b5d9b8e4eecb9aef3958818bbc5c38
EXPECTED_CERT_SHA256: 69fe7fdd7748c2faddbef43c66bc8becf719aaaccdd340de42f7f3eb314b4d11
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
Expand All @@ -81,7 +81,7 @@ jobs:
- name: Install NDK
run: |
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358" "cmake;3.31.6" > /dev/null
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358" "cmake;3.31.6" "build-tools;36.0.0" > /dev/null

- name: Signing key
env:
Expand All @@ -91,7 +91,55 @@ jobs:
KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
if [ -n "$KEYSTORE_BASE64" ]; then
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
# Tolerate how the secret was pasted: line endings, wrapping, spaces, invisible
# characters (BOM, zero-width space) and "-----BEGIN/END ...-----" lines (certutil).
# Opening the keystore and the certificate check below still guard the release.
CLEAN=$(printf '%s\n' "$KEYSTORE_BASE64" | tr -d '\r' | grep -v -- '-----' | tr -cd 'A-Za-z0-9+/=')
DROPPED=$(( $(printf '%s' "$KEYSTORE_BASE64" | wc -c) - ${#CLEAN} ))
if [ "$DROPPED" -gt 0 ]; then echo "Ignored $DROPPED stray bytes (whitespace or invisible characters) in SIGNING_KEYSTORE_BASE64."; fi
if ! printf '%s' "$CLEAN" | base64 -d > "$RUNNER_TEMP/release.jks" 2>/dev/null; then
echo "::error::SIGNING_KEYSTORE_BASE64 is not valid base64 (${#CLEAN} base64 characters). Re-create it with 'base64 -w0 release.jks' and paste it again."
exit 1
fi
# Same for the other secrets: drop a BOM, zero-width spaces, line breaks and
# surrounding spaces.
tidy() { printf '%s' "$1" | sed -e 's/\xef\xbb\xbf//g; s/\xe2\x80\x8b//g' | tr -d '\r\n' | sed -e 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
STORE_PASSWORD=$(tidy "$STORE_PASSWORD"); KEY_PASSWORD=$(tidy "$KEY_PASSWORD"); KEY_ALIAS=$(tidy "$KEY_ALIAS")
KS="$RUNNER_TEMP/release.jks"
if ! keytool -list -keystore "$KS" -storepass "$STORE_PASSWORD" > /dev/null 2>&1; then
if [ -n "$KEY_PASSWORD" ] && keytool -list -keystore "$KS" -storepass "$KEY_PASSWORD" > /dev/null 2>&1; then
echo "::warning::SIGNING_KEY_PASSWORD opens the keystore but SIGNING_STORE_PASSWORD does not; the two look swapped. Using it for both."
STORE_PASSWORD="$KEY_PASSWORD"
else
WHY=$(keytool -list -keystore "$KS" -storepass "$STORE_PASSWORD" 2>&1 | grep -i 'keytool error' | head -1)
echo "::error::The keystore decoded ($(wc -c < "$KS") bytes, $(file -b "$KS" | cut -c1-40)) but SIGNING_STORE_PASSWORD does not open it. keytool says: $WHY"
exit 1
fi
fi
ALIASES=$(keytool -list -v -keystore "$KS" -storepass "$STORE_PASSWORD" 2>/dev/null | sed -n 's/^Alias name: //p')
if ! printf '%s\n' "$ALIASES" | grep -qixF -- "$KEY_ALIAS"; then
if [ "$(printf '%s\n' "$ALIASES" | grep -c .)" -eq 1 ]; then
echo "::warning::SIGNING_KEY_ALIAS does not match the key in the keystore; using its only key."
KEY_ALIAS="$ALIASES"
else
echo "::error::SIGNING_KEY_ALIAS does not match any of the $(printf '%s\n' "$ALIASES" | grep -c .) keys in the keystore."
exit 1
fi
fi
# The key password must unlock the private key. In a PKCS12 keystore it is always
# the store password (and keytool ignores -keypass), so use that.
if keytool -list -keystore "$KS" -storepass "$STORE_PASSWORD" 2>/dev/null | grep -qi '^Keystore type: PKCS12'; then
if [ "$KEY_PASSWORD" != "$STORE_PASSWORD" ]; then echo "PKCS12 keystore: signing with the store password as the key password."; fi
KEY_PASSWORD="$STORE_PASSWORD"
elif ! keytool -certreq -keystore "$KS" -storepass "$STORE_PASSWORD" -alias "$KEY_ALIAS" -keypass "$KEY_PASSWORD" > /dev/null 2>&1; then
if keytool -certreq -keystore "$KS" -storepass "$STORE_PASSWORD" -alias "$KEY_ALIAS" -keypass "$STORE_PASSWORD" > /dev/null 2>&1; then
echo "::warning::SIGNING_KEY_PASSWORD does not unlock the key, but the store password does; using that."
KEY_PASSWORD="$STORE_PASSWORD"
else
echo "::error::SIGNING_KEY_PASSWORD does not unlock the key. Check that secret."
exit 1
fi
fi
{
echo "SIGNING_STORE_FILE=$RUNNER_TEMP/release.jks"
echo "SIGNING_STORE_PASSWORD=$STORE_PASSWORD"
Expand Down Expand Up @@ -120,10 +168,17 @@ jobs:

- name: Verify signatures
run: |
APKSIGNER="$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1)apksigner"
# A pinned apksigner: the newest one on the runner printed no certificate.
APKSIGNER="$ANDROID_HOME/build-tools/36.0.0/apksigner"
for f in dist/*.apk; do
CERT=$("$APKSIGNER" verify --print-certs "$f" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')
echo "$(basename "$f"): $CERT"
OUT=$("$APKSIGNER" verify --print-certs "$f" 2>&1) || { echo "$OUT"; echo "::error::$(basename "$f") does not verify."; exit 1; }
CERT=$(printf '%s\n' "$OUT" | grep -oiE 'certificate SHA-256 digest: [0-9a-f]{64}' | head -1 | awk '{print tolower($NF)}')
echo "$(basename "$f"): ${CERT:-<none>}"
if [ -z "$CERT" ]; then
echo "$OUT"
echo "::error::Could not read the signing certificate of $(basename "$f")."
exit 1
fi
if [ "$CERT" != "$EXPECTED_CERT_SHA256" ] && [ "$TEST_BUILD" != "true" ]; then
echo "::error::$(basename "$f") is not signed with the Meshwise release key."
exit 1
Expand Down Expand Up @@ -156,7 +211,7 @@ jobs:
Every APK is signed with the Meshwise release certificate. You can check it with
`apksigner verify --print-certs <file>.apk`. The SHA-256 must be:
```
CB:EF:EB:0E:76:73:75:BE:72:D2:93:25:F9:7E:C6:02:B4:B5:D9:B8:E4:EE:CB:9A:EF:39:58:81:8B:BC:5C:38
69:FE:7F:DD:77:48:C2:FA:DD:BE:F4:3C:66:BC:8B:EC:F7:19:AA:AC:CD:D3:40:DE:42:F7:F3:EB:31:4B:4D:11
```
File checksums are in `SHA256SUMS.txt`.
NOTES
Expand Down
22 changes: 15 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
# Changelog

All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
All notable changes to Meshwise are listed here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
[semantic versioning](https://semver.org/).

## [1.0.0] - 2026-10-13

First public release.

### Added
- Native loaders for binary/ASCII STL, OBJ (+MTL colors), 3MF (multi-object, colors), PLY
(binary/ASCII, vertex colors), OFF and ZIP archives; STEP, IGES and BREP through Open CASCADE
in an isolated process with a tessellation cache.
- Native loaders for binary and ASCII STL, OBJ (with MTL colors), 3MF (multiple objects,
colors), PLY (binary and ASCII, vertex colors), OFF and ZIP archives. STEP, IGES and BREP
through Open CASCADE, in a separate process with a tessellation cache.
- OpenGL ES 3 renderer: physically based materials, studio/outdoor/flat lighting, soft shadows,
feature edges, wireframe, x-ray, hatched section caps and an interactive section plane.
Big models upload to the GPU without blocking touch input.
- Tools: section analysis, explode (automatic and manual with gizmo and undo), parts list with
search/isolate/focus, colors and materials, measuring, dimensions overlay, wall-thickness and
overhang analysis, mesh check, weight and cost estimate.
- Projects (`.meshwise`), export to STL, OBJ, 3MF and PNG, on-device library, recent files with
thumbnails, five sample models.
- Ko-fi support links in Settings, About and on the home screen (they open in the browser; the
app itself has no network access).
- Optional update check. On first launch the app asks whether it may check GitHub for new
releases (off unless the user agrees; switch and "Check now" in Settings). It asks the GitHub
releases API at most once a day, sends nothing else, and offers the APK for the device's ABI
with Download, Later or Skip this version. This is the only use of the `INTERNET` permission.
- Launcher icon: a faceted gem with an orange hatched section cut, plus a themed (monochrome)
version, generated by `scripts/make_icon.py`.
- Ko-fi support links in Settings, About and on the home screen (they open in the browser).
28 changes: 16 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
# Contributing

Thanks for helping improve Meshwise! Bug reports, model files that load wrongly, and pull
requests are all welcome.
Thanks for helping improve Meshwise! Bug reports, files that load wrongly, and pull requests
are all welcome.

## Reporting a problem

Open an issue with your device model, Android version, the app version (Settings → About) and,
if a file does not open or looks wrong, the file itself or a link to it when you can share it.
[Open an issue](../../issues/new/choose) and include:

- your phone or tablet model and Android version
- the app version (Settings → About)
- if a file doesn't open or looks wrong: the file, or a link to it, if you can share it

## Development setup

See [Building](README.md#building) and [Testing](README.md#testing) in the README. The fastest
loop for anything in `native/core` is the desktop build: `fsv_tests` for logic and
`fsv_render` to look at rendering changes without a phone.
See [Building](README.md#building) and [Testing](README.md#testing) in the README.

For anything in `native/core`, the desktop build is the fastest loop: `fsv_tests` checks the
logic and `fsv_render` shows rendering changes without a phone.

## Pull requests

- Keep changes focused; one feature or fix per pull request.
- Run `native/build-host/fsv_tests` and `./gradlew :app:testDebugUnitTest` before pushing.
- Keep each pull request to one feature or fix.
- Run `native/build-host/fsv_tests` and `./gradlew :app:testDebugUnitTest` before you push.
- Add a test in `native/tests/tests.cpp` for loader, geometry or analysis changes.
- UI changes: regenerate the README screenshots with `scripts/make_screenshots.sh` when they
change what the screenshots show.
- If a UI change affects what the README screenshots show, regenerate them with
`scripts/make_screenshots.sh`.
- Match the surrounding style: C++17 in `native/`, idiomatic Kotlin and Compose in `app/`.

By contributing you agree that your contributions are licensed under the MIT License.
By contributing, you agree that your contributions are licensed under the MIT License.
Loading
Loading