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
19 changes: 15 additions & 4 deletions .github/scripts/bundle-engines.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ set -euo pipefail
ADDON_DIR="$1"
ENGINE_ZIP_PREFIX="$2"

# the binary name is the prefix without the -engine suffix (optcuts, xatlas)
engine_bin_name="${ENGINE_ZIP_PREFIX%-engine}"

engine_zips=( ${ENGINE_ZIP_PREFIX}-*.zip )
if [ ! -f "${engine_zips[0]}" ]; then
echo "No engine zips found matching ${ENGINE_ZIP_PREFIX}-*.zip"
Expand All @@ -23,8 +26,8 @@ for zip_file in "${engine_zips[@]}"; do
tmp_dir=$(mktemp -d)
unzip -o "$zip_file" -d "$tmp_dir"

# find the engine binary (named optcuts or optcuts.exe)
engine_bin=$(find "$tmp_dir" -name "optcuts" -o -name "optcuts.exe" | head -1)
# find the engine binary (named e.g. optcuts or optcuts.exe)
engine_bin=$(find "$tmp_dir" -name "$engine_bin_name" -o -name "${engine_bin_name}.exe" | head -1)
if [ -n "$engine_bin" ]; then
mkdir -p "${ADDON_DIR}/engines/${platform}"
cp "$engine_bin" "${ADDON_DIR}/engines/${platform}/"
Expand All @@ -36,9 +39,10 @@ for zip_file in "${engine_zips[@]}"; do
rm -rf "$tmp_dir"
done

# ship the license notices for the optcuts binary and everything statically
# linked into it, next to the binaries. mit and apache-2.0 require the notice
# ship the license notices for both engine binaries and everything statically
# linked into them, next to the binaries. mit and apache-2.0 require the notice
# travel with the binary; mpl-2.0 requires the notice plus source availability.
# runs on every invocation and covers both engines, so it stays idempotent.
license_dir="${ADDON_DIR}/engines/licenses"
mkdir -p "$license_dir"
cp engine/optcuts/LICENSE.txt "$license_dir/OptCuts-LICENSE-MIT.txt"
Expand All @@ -48,6 +52,8 @@ cp engine/optcuts/ext/libigl/LICENSE.MPL2 "$license_dir/libigl-LICENSE-MPL2.txt"
# engine build treats any diff there as needing a version bump.
cp .github/licenses/oneTBB-LICENSE-Apache2.txt "$license_dir/"
cp .github/licenses/mimalloc-LICENSE-MIT.txt "$license_dir/"
# xatlas is dependency-free and links nothing extra
cp engine/xatlas/LICENSE "$license_dir/xatlas-LICENSE-MIT.txt"

cat > "$license_dir/README.txt" <<'EOF'
The UVgami OptCuts engine binary (engines/<platform>/optcuts[.exe]) statically
Expand All @@ -59,6 +65,11 @@ links these components. Their license texts are in this folder.
oneTBB Apache-2.0 oneTBB-LICENSE-Apache2.txt
mimalloc MIT mimalloc-LICENSE-MIT.txt

The UVgami xatlas engine binary (engines/<platform>/xatlas[.exe]) is
dependency-free and links no other components.

xatlas MIT xatlas-LICENSE-MIT.txt

The UVgami add-on itself is GPL-3.0-or-later (see the add-on's LICENSE).
EOF
echo "Bundled engine license notices"
40 changes: 27 additions & 13 deletions .github/workflows/engine-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@ name: Engine build

on:
workflow_dispatch:
inputs:
engine:
description: Engine to build
type: choice
options:
- optcuts
- xatlas
default: optcuts
workflow_call:
inputs:
engine:
type: string
default: optcuts
outputs:
built:
value: ${{ jobs.prepare.outputs.built }}
Expand All @@ -12,7 +24,8 @@ permissions:

env:
BUILD_TYPE: Release
PACKAGE_PREFIX: optcuts-engine
ENGINE: ${{ inputs.engine }}
PACKAGE_PREFIX: ${{ inputs.engine }}-engine
ADDON_NAME: "UVgami"

jobs:
Expand All @@ -29,11 +42,11 @@ jobs:
id: release
shell: bash
run: |
VERSION=$(tr -d '[:space:]' < engine/optcuts/VERSION)
TAG="optcuts-v$VERSION"
VERSION=$(tr -d '[:space:]' < engine/$ENGINE/VERSION)
TAG="$ENGINE-v$VERSION"
if git rev-parse --quiet --verify "refs/tags/$TAG" >/dev/null; then
if ! git diff --quiet "$TAG" HEAD -- engine/optcuts; then
echo "::error::OptCuts changed since $TAG. Bump engine/optcuts/VERSION."
if ! git diff --quiet "$TAG" HEAD -- engine/$ENGINE; then
echo "::error::$ENGINE changed since $TAG. Bump engine/$ENGINE/VERSION."
exit 1
fi
echo "built=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -64,15 +77,16 @@ jobs:
id: engine_version
shell: bash
run: |
VERSION=$(cat engine/optcuts/VERSION | tr -d '[:space:]')
VERSION=$(cat engine/$ENGINE/VERSION | tr -d '[:space:]')
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "major=$MAJOR" >> $GITHUB_OUTPUT
echo "minor=$MINOR" >> $GITHUB_OUTPUT
echo "patch=$PATCH" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT

# optcuts links glfw/opengl; xatlas is dependency-free
- name: Install Ubuntu dependencies
if: startsWith(matrix.os, 'ubuntu')
if: startsWith(matrix.os, 'ubuntu') && inputs.engine == 'optcuts'
run: |
sudo apt-get update -y
sudo apt-get install -y xorg-dev libglu1-mesa-dev
Expand All @@ -91,7 +105,7 @@ jobs:
working-directory: ${{ github.workspace }}/build
shell: bash
run: |
cmake $GITHUB_WORKSPACE/engine/optcuts -Wno-dev -DBUILD_SHARED_LIBS=OFF \
cmake $GITHUB_WORKSPACE/engine/$ENGINE -Wno-dev -DBUILD_SHARED_LIBS=OFF \
-DCPACK_GENERATOR:STRING=ZIP \
-DCPACK_PACKAGE_FILE_NAME:STRING=$PACKAGE_NAME

Expand All @@ -100,7 +114,7 @@ jobs:
- name: Upload engine artifact
uses: actions/upload-artifact@v4
with:
name: engine-${{ matrix.pkg_name }}
name: ${{ inputs.engine }}-${{ matrix.pkg_name }}
path: build/${{ env.PACKAGE_NAME }}.zip

# publish the canonical engine release the addon and its bundled zip source from
Expand All @@ -117,19 +131,19 @@ jobs:
- name: Read engine version
id: engine_version
shell: bash
run: echo "version=$(cat engine/optcuts/VERSION | tr -d '[:space:]')" >> $GITHUB_OUTPUT
run: echo "version=$(cat engine/$ENGINE/VERSION | tr -d '[:space:]')" >> $GITHUB_OUTPUT

- name: Download all engine artifacts
uses: actions/download-artifact@v4
with:
pattern: engine-*
pattern: ${{ inputs.engine }}-*
merge-multiple: true

- name: Publish engine release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: optcuts-v${{ steps.engine_version.outputs.version }}
TAG: ${{ inputs.engine }}-v${{ steps.engine_version.outputs.version }}
run: |
gh release view "$TAG" >/dev/null 2>&1 \
|| gh release create "$TAG" --latest=false --title "$TAG" --notes "OptCuts engine binaries"
|| gh release create "$TAG" --latest=false --title "$TAG" --notes "$ENGINE engine binaries"
gh release upload "$TAG" ${PACKAGE_PREFIX}-*.zip --clobber
40 changes: 33 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ on:
env:
ADDON_NAME: "UVgami"
INCLUDED_FILES: "src LICENSE __init__.py blender_manifest.toml"
ENGINE_PREFIX: optcuts-engine

jobs:
check-version:
Expand Down Expand Up @@ -47,6 +46,17 @@ jobs:
secrets: inherit
permissions:
contents: write
with:
engine: optcuts

xatlas:
needs: prepare
uses: ./.github/workflows/engine-build.yml
secrets: inherit
permissions:
contents: write
with:
engine: xatlas

partuv:
needs: prepare
Expand All @@ -56,7 +66,7 @@ jobs:
contents: write

package:
needs: [prepare, optcuts, partuv]
needs: [prepare, optcuts, xatlas, partuv]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -66,20 +76,35 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6

- name: Download built engines
- name: Download built optcuts
if: needs.optcuts.outputs.built == 'true'
uses: actions/download-artifact@v4
with:
pattern: engine-*
pattern: optcuts-*
merge-multiple: true

- name: Download released engines
- name: Download built xatlas
if: needs.xatlas.outputs.built == 'true'
uses: actions/download-artifact@v4
with:
pattern: xatlas-*
merge-multiple: true

- name: Download released optcuts
if: needs.optcuts.outputs.built != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ENGINE_VERSION=$(cat engine/optcuts/VERSION | tr -d '[:space:]')
gh release download "optcuts-v$ENGINE_VERSION" -p "${ENGINE_PREFIX}-*.zip"
gh release download "optcuts-v$ENGINE_VERSION" -p "optcuts-engine-*.zip"

- name: Download released xatlas
if: needs.xatlas.outputs.built != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ENGINE_VERSION=$(cat engine/xatlas/VERSION | tr -d '[:space:]')
gh release download "xatlas-v$ENGINE_VERSION" -p "xatlas-engine-*.zip"

- name: Move to nested folder
run: |
Expand All @@ -88,7 +113,8 @@ jobs:

- name: Bundle engines and zip
run: |
bash .github/scripts/bundle-engines.sh $ADDON_NAME $ENGINE_PREFIX
bash .github/scripts/bundle-engines.sh $ADDON_NAME optcuts-engine
bash .github/scripts/bundle-engines.sh $ADDON_NAME xatlas-engine
zip -r ${ADDON_NAME}.zip $ADDON_NAME

- name: Build extension zips
Expand Down
11 changes: 5 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# UVgami

Blender addon that auto unwraps UVs. Two engines: optcuts (bundled C++ binary) and partuv (CUDA wheel in `engine/partuv`, runs as `python -m partuv`).
Blender addon that auto unwraps UVs. Three engines: optcuts and xatlas (bundled C++ binaries) and partuv (CUDA wheel in `engine/partuv`, runs as `python -m partuv`).

## Layout

- `src/`: the addon. `manager.py` runs the unwrap queue, `src/engines/` is one module per engine (optcuts, partuv), discovered at import. Deleting `src/engines/partuv/` removes that engine cleanly.
- `src/`: the addon. `manager.py` runs the unwrap queue, `src/engines/` is one module per engine (optcuts, xatlas, partuv), discovered at import. Deleting `src/engines/partuv/` removes that engine cleanly.
- `engine/partuv/`: the partuv wheel, C++ core plus python driver (`partuv/cli.py`).
- `uvgami_cli/`: dev-only CLI driving both engines via `--engine`, not shipped.
- `uvgami_cli/`: dev-only CLI driving every engine via `--engine`, not shipped.
- `docs/docs.md`: user guide, with a development section at the end. `docs/agents/partuv-packaging.md`: packaging decisions and open questions.

## Commands

- Test: `uv run --no-sync pytest` (no GPU or Blender needed)
- Lint: `uv run --no-sync ruff check --fix` then `uv run --no-sync ruff format`
- Use uv for everything, never pip.

## Gotchas

Expand All @@ -23,5 +22,5 @@ Blender addon that auto unwraps UVs. Two engines: optcuts (bundled C++ binary) a
- Never add a blocking stdin reader thread to the partuv CLI. On Windows a thread stuck reading stdin stalls native DLL imports.
- Engine stdout is a parsed protocol (`start:`/`done:`/`failed:`/`progress:` lines). Don't print extra lines to stdout in the engine path, use stderr.
- `src/` imports bpy, so only bpy-free modules (`src/batch.py`, the partuv package) are unit-testable.
- When you change an engine's code, bump that engine's version: optcuts in `engine/optcuts/VERSION`, partuv in `engine/partuv/pyproject.toml` (mirror it in `src/engines/partuv/install.py` `PARTUV_VERSION`, `check-partuv-version.yml` fails the build if they drift). That rebuilds the engine only. It does not cut an addon release: a release triggers solely from bumping the version line in `blender_manifest.toml`.
- After building optcuts, copy the binary to the dev engines folder or the addon runs the stale bundled one: `engine/optcuts/build-perf/optcuts.exe` -> `engines/windows/optcuts.exe` (per-platform under `engines/`, gitignored).
- Changing an engine's code needs that engine's version bumped: `engine/optcuts/VERSION`, `engine/xatlas/VERSION`, `engine/partuv/pyproject.toml` (mirrored in `src/engines/partuv/install.py` `PARTUV_VERSION`, `check-partuv-version.yml` fails on drift). That rebuilds the engine only. Releases trigger solely from the version line in `blender_manifest.toml`.
- After building an engine, copy the binary to the dev engines folder or the addon runs the stale bundled one: `engine/optcuts/build-perf/optcuts.exe` and `engine/xatlas/build/Release/xatlas.exe` -> `engines/windows/` (per-platform, gitignored).
7 changes: 5 additions & 2 deletions docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Blender 2.9+

- [Installation](#installation)
- [Add-on](#add-on)
- [xatlas Engine](#xatlas-engine)
- [PartUV Engine](#partuv-engine)
- [Instructions](#instructions)
- [Unwrap a Mesh](#unwrap-a-mesh)
Expand Down Expand Up @@ -72,8 +73,6 @@ Blender 2.9+

## Installation

Download the add-on [here](https://github.com/DanielBoxer/UVgami/releases/latest)

### Add-on

- Download `UVgami.zip` (don't extract it)
Expand All @@ -84,6 +83,10 @@ To use a different OptCuts build instead of the bundled one, select it with the

![Engine Path](img/engine_path.jpg)

### xatlas Engine

xatlas is bundled and auto detected like OptCuts, so there's nothing to install. It's a fast unwrapper with no settings to tune.

### PartUV Engine

PartUV needs CUDA and runs on Windows or Linux. Install the add-on first, then in UVgami preferences click `Install PartUV Engine`. It downloads the engine and installs it into a managed Python venv.
Expand Down
1 change: 1 addition & 0 deletions engine/xatlas/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build/
33 changes: 33 additions & 0 deletions engine/xatlas/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.15)
project(xatlas)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# static msvc runtime so the exe ships without vc redist, matching optcuts.
if(MSVC)
set(CMAKE_POLICY_DEFAULT_CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release)
endif()

add_executable(xatlas main.cpp xatlas.cpp)

if(MSVC)
target_compile_options(xatlas PRIVATE $<$<CONFIG:Release>:/O2>)
else()
target_compile_options(xatlas PRIVATE $<$<CONFIG:Release>:-O3>)
# xatlas uses std::thread
find_package(Threads REQUIRED)
target_link_libraries(xatlas PRIVATE Threads::Threads)
endif()

# package the exe into a zip. the ci passes -DCPACK_GENERATOR=ZIP and
# -DCPACK_PACKAGE_FILE_NAME; bundle-engines.sh finds the binary by name.
install(TARGETS xatlas RUNTIME DESTINATION .)
set(CPACK_PACKAGE_NAME xatlas)
include(CPack)
21 changes: 21 additions & 0 deletions engine/xatlas/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018-2020 Jonathan Young

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions engine/xatlas/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
Loading
Loading