Compare commits

..

2 Commits

Author SHA1 Message Date
nan
8c037fc8a9 Add documentation deployment workflow and update feature installation instructions
Some checks failed
构建扩展 / build (push) Failing after 14s
构建并部署文档 / deploy (push) Failing after 5m16s
2026-08-19 20:02:01 +08:00
nan
9fb0d010e3 Bump version to 0.12.15 in project and blender manifest files; add Apple Silicon support in properties.py 2026-08-19 20:01:52 +08:00
10 changed files with 173 additions and 18 deletions

56
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: 构建并部署文档
on:
push:
branches:
- main
paths:
- docs/**
- mkdocs.yml
- pyproject.toml
- uv.lock
- .github/workflows/docs.yml
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v6
- name: 配置 uv
uses: astral-sh/setup-uv@v7
- name: 构建文档
run: uv run --frozen mkdocs build --strict --site-dir site
- name: 整理发布目录
run: |
mkdir -p public/snapform
cp -a site/. public/snapform/
- name: 配置 Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
- name: 部署到 Cloudflare Workers
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_WORKER_NAME: ${{ vars.CLOUDFLARE_WORKER_NAME }}
run: |
test -n "$CLOUDFLARE_ACCOUNT_ID"
test -n "$CLOUDFLARE_API_TOKEN"
test -n "$CLOUDFLARE_WORKER_NAME"
npx --yes wrangler@4 deploy \
--assets ./public \
--name "$CLOUDFLARE_WORKER_NAME" \
--compatibility-date 2026-08-19 \
--keep-vars \
--message "SnapForm docs $GITHUB_SHA" \
--autoconfig=false

View File

@@ -20,7 +20,7 @@ SnapForm 是一个 Blender 扩展,可以把一张照片、一段视频或一
2. 打开 Blender,进入 `Preferences > Extensions`
3. 点击 `Install from Disk`,选择下载的压缩包
4. 在扩展偏好设置中确认 Storage Root 与 Device(保持默认即可);Device 默认取本机
最快的 DirectML/CoreML provider,否则 CPU
最快的 DirectML provider;Apple M4 或更新芯片可选 MPS/CoreML,其他 Mac 只提供 CPU
5. 打开 3D 视图右侧的 `SnapForm` 面板:使用官方源点击 `Install Environment`,使用国内镜像点击 `Install Environment (Mirror)`
扩展不内置 uv 或 AI 运行库。首次使用会先下载并校验固定版本的 uv,随后自动安装轻量的 ONNX Runtime 环境,再下载 `MoGe-2 ViT-S Normal` 和
@@ -68,7 +68,7 @@ Bump 合并;网格本身不写入 custom normal,因此重拓扑降低顶点
当前此功能仅支持单张图片。
MoGe-2 ViT-S、ViT-B、ViT-L、DA3 Small、DA3 Base 与 BEN2 全部使用 ONNX。模型执行、相机与深度恢复、
点云重投影和网格生成均不依赖 PyTorch。Windows 使用 DirectML 加速,macOS 使用 CoreML 的 `MLProgram` 模型格式;provider 不可用时会回退到 ONNX Runtime CPU,
点云重投影和网格生成均不依赖 PyTorch。Windows 使用 DirectML 加速;Apple M4 或更新芯片可使用 CoreML 的 `MLProgram` 模型格式,较早或无法识别的 Mac 只提供 CPU;provider 不可用时会回退到 ONNX Runtime CPU,
并在任务状态中显示实际 provider。
转换对话框提供模型与 `Create Matching Camera`、`Remove Background`、`Shadeless`;DA3 显示

View File

@@ -123,6 +123,24 @@ uv run mike deploy <version> -p
uv run mike set-default <version> -p
```
### 文档部署
`.github/workflows/docs.yml` 在 `main` 分支的文档相关文件变化后运行,也可从 Gitea Actions 手动触发。工作流使用锁定的 Python 依赖严格构建 `site/`,将其放入与站点 URL 对应的 `public/snapform/`,再通过 Wrangler 部署为 Cloudflare Workers Static Assets。
在 Gitea 仓库的 **Settings > Actions** 中配置:
- Secret `CLOUDFLARE_ACCOUNT_ID`:Cloudflare Account ID
- Secret `CLOUDFLARE_API_TOKEN`:限制到目标账号的 Cloudflare API Token,使用 **Edit Cloudflare Workers** 模板
- Variable `CLOUDFLARE_WORKER_NAME`:承载文档的 Worker 名称
API Token 不写入仓库。Workflow 只在 `main` 上部署生产文档,不在 Pull Request 中读取 Cloudflare Secret。
目标 Worker 必须专门用于 SnapForm,并把 `docs.omoolab.xyz/snapform/*` 路由到该 Worker。每次 Wrangler 部署都会更新目标 Worker 的整套静态资源;如果一个 Worker 同时承载多个项目,应改由中央文档仓库聚合全部站点后统一部署,不能从各项目仓库分别覆盖它。
Mike 不依赖 GitHub API;`mike deploy --branch <branch> --push` 也可以向 Gitea 保存版本化静态站点。当前自动部署先发布 `mkdocs build` 生成的单版本站点,等确定版本 URL 规则后再启用 Mike 分支,避免在 Cloudflare 上提前固化不合适的路径结构。
`pack.py` 根据 `blender_manifest.toml` 分别生成 Windows、macOS 和 Linux 扩展包,例如 `dist/SnapForm.v0.12.14.windows-x64.zip`。更换 `src/snapform/wheels/` 中随扩展分发的 wheel 后,先刷新锁文件再测试:

View File

@@ -53,12 +53,11 @@
## 使用
1. 运行 `uv run python pack.py`,分别构建 `windows-x64`、`macos-arm64`、
`linux-x64` 三个平台的 Extension ZIP(如 `SnapForm.v0.12.14.macos-arm64.zip`)。
`linux-x64` 三个平台的 Extension ZIP(如 `SnapForm.v0.12.17.macos-arm64.zip`)。
安装包不携带 uv 或 AI 项目源码;首次安装 Environment 时下载固定版本 uv,并校验固定 SHA-256。官方安装按钮使用 GitHub、uv 默认 Python 下载源和 PyPI;镜像按钮使用 CNB uv Release、CNB Python 解释器镜像和清华 PyPI。
2. 在 Blender 的 `Preferences > Extensions` 中选择 `Install from Disk`。
3. 在 Extension Preferences 中确认 Storage Root 与 Device。Device 是正式功能
(Image Empty 右键菜单)使用的推理设备,默认取本机最快的 DirectML/CoreML provider,否则 CPU,且只列出
本机支持的设备。
(Image Empty 右键菜单)使用的推理设备。Windows 默认取 DirectML,否则 CPU;Apple M4 或更新芯片列出 MPS/CoreML 与 CPU,Intel、M1–M3 和无法识别的 Mac 只列出 CPU。
4. 在 `Job Server` 选择 `Install Environment` 或 `Install Environment (Mirror)`。首次安装会下载 Python、ONNX Runtime、FastAPI、Uvicorn;
uv 由 Job Runtime 的 Environment 安装器下载并校验,DA3、BEN2 与 MoGe 不安装源码包,
并在 Environment 安装后自动下载 `MoGe-2 ViT-S Normal` 和 FP16 `BEN2 Base` 推荐模型。首次安装

View File

@@ -66,7 +66,7 @@ ONNX 实现位于 `models/`;纯投影、网格与唯一 Surface Builder 位于
运行环境不安装 PyTorch、TorchVision、xFormers,也不安装 DA3、MoGe、BEN2 或 utils3d 的 Python 源码包。模型推理和 SnapForm 所需的相机、点云、网格后处理均由 ONNX Runtime、NumPy 与内部实现完成。
Windows 当前发行环境提供 DirectML 与 CPU;`Auto` 优先使用 `DmlExecutionProvider`。显式指定不可用设备时会安全回退到 CPU。其他平台安装标准 ONNX Runtime,并按可用 provider 选择 CoreML、OpenVINO 或 CPU。macOS 的 CoreML Session 使用 `MLProgram` 模型格式与全部可用计算单元,允许动态输入 Shape;未被 CoreML 接管的节点继续由 CPU 执行。
Windows 当前发行环境提供 DirectML 与 CPU;`Auto` 优先使用 `DmlExecutionProvider`。显式指定不可用设备时会安全回退到 CPU。其他平台安装标准 ONNX Runtime,并按可用 provider 选择 CoreML、OpenVINO 或 CPU。Blender 侧通过 `sysctl` 识别 Apple Silicon 代际,只有 M4 或更新芯片才提供 MPS 选项;其他 Mac 只提供 CPU。M4 或更新芯片的 CoreML Session 使用 `MLProgram` 模型格式、全部可用计算单元并允许动态输入 Shape。
`environment.json` 保存规范化后的声明,`manifest.json` 保存该声明与基础 Server packages 的 Hash。Python、packages、平台 packages、基础 Server packages 或 Environment Schema 改变后需要重新执行 `Install Environment`,不再维护手工 Manifest Revision。BlendJob 直接随 Extension 更新,不参与 Environment Hash。

View File

@@ -1,6 +1,6 @@
[project]
name = "snapform"
version = "0.12.14"
version = "0.12.15"
description = "Fast image-guided structure building tools for Blender"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "snapform"
version = "0.12.14"
version = "0.12.15"
name = "SnapForm"
tagline = "Build spatial structures from images, fast"
maintainer = "OmooLab"

View File

@@ -1,4 +1,7 @@
import functools
import platform
import re
import subprocess
import sys
import bpy
@@ -21,18 +24,50 @@ DEVICE_LABELS = {
}
def supported_devices():
devices = runtime.environment_manifest().get("devices")
if isinstance(devices, list) and devices:
return [str(device).lower() for device in devices]
if sys.platform == "win32":
return ["directml", "cpu"]
if sys.platform == "darwin" and platform.machine().lower() in {
APPLE_SILICON_PATTERN = re.compile(r"\bApple M(\d+)\b", re.IGNORECASE)
def parse_apple_silicon_generation(brand):
match = APPLE_SILICON_PATTERN.search(str(brand))
return int(match.group(1)) if match else None
@functools.lru_cache(maxsize=1)
def apple_silicon_generation():
if sys.platform != "darwin" or platform.machine().lower() not in {
"arm64",
"aarch64",
}:
return ["mps", "cpu"]
return ["cpu"]
return None
try:
result = subprocess.run(
("sysctl", "-n", "machdep.cpu.brand_string"),
check=True,
capture_output=True,
text=True,
timeout=2,
)
except (OSError, subprocess.SubprocessError):
return None
return parse_apple_silicon_generation(result.stdout)
def supports_mps(generation=None):
if generation is None:
generation = apple_silicon_generation()
return generation is not None and generation >= 4
def supported_devices():
devices = runtime.environment_manifest().get("devices")
if isinstance(devices, list) and devices:
names = [str(device).lower() for device in devices]
if not supports_mps():
names = [name for name in names if name != "mps"]
return names or ["cpu"]
if sys.platform == "win32":
return ["directml", "cpu"]
return ["mps", "cpu"] if supports_mps() else ["cpu"]
def device_enum_items(_owner, _context):

View File

@@ -124,6 +124,53 @@ class AddonTest(unittest.TestCase):
self.assertFalse((SOURCE_ROOT / "snapform" / name).exists(), name)
self.assertFalse(hasattr(self.addon, "client"))
def test_mps_requires_m4_or_newer(self):
properties = self.addon.properties
parse = properties.parse_apple_silicon_generation
self.assertEqual(parse("Apple M2"), 2)
self.assertEqual(parse("Apple M4 Pro"), 4)
self.assertEqual(parse("Apple M10 Max"), 10)
self.assertIsNone(parse("Intel(R) Core(TM) i9"))
self.assertFalse(properties.supports_mps(3))
self.assertTrue(properties.supports_mps(4))
def test_unsupported_mac_hides_manifest_mps_device(self):
properties = self.addon.properties
manifest = {"devices": ["mps", "cpu"]}
with (
patch.object(
self.addon.runtime,
"environment_manifest",
return_value=manifest,
),
patch.object(properties, "supports_mps", return_value=False),
):
self.assertEqual(properties.supported_devices(), ["cpu"])
with (
patch.object(
self.addon.runtime,
"environment_manifest",
return_value=manifest,
),
patch.object(properties, "supports_mps", return_value=True),
):
self.assertEqual(properties.supported_devices(), ["mps", "cpu"])
def test_windows_keeps_default_directml_device(self):
properties = self.addon.properties
with (
patch.object(
self.addon.runtime,
"environment_manifest",
return_value={},
),
patch.object(properties.sys, "platform", "win32"),
):
self.assertEqual(
properties.supported_devices(),
["directml", "cpu"],
)
def test_import_does_not_add_addon_directories_to_sys_path(self):
self.assertEqual(self.paths_after_import, self.paths_before_import)
self.assertFalse(

2
uv.lock generated
View File

@@ -858,7 +858,7 @@ wheels = [
[[package]]
name = "snapform"
version = "0.12.14"
version = "0.12.15"
source = { virtual = "." }
dependencies = [
{ name = "blendjob" },