Add SnapForm panels, preferences, properties, and runtime management

- Implemented RuntimeModelsPanel and DebugPanel for UI integration in Blender.
- Created SnapFormPreferences to manage user settings for storage and runtime configuration.
- Defined SnapFormSettings to hold various properties related to input, model selection, and export options.
- Developed runtime management functions for installation, model downloading, and reconstruction commands.
- Added status bar functionality to display job progress and status in the Blender UI.
- Implemented worker script for handling model downloads and depth reconstruction tasks.
- Introduced unit tests for download progress reporting and image depth extraction functionalities.
This commit is contained in:
2026-08-03 10:13:33 +08:00
parent 9586b35440
commit b02372c457
26 changed files with 1849 additions and 181 deletions

218
src/snapform/runtime.py Normal file
View File

@@ -0,0 +1,218 @@
import json
import os
import shutil
import subprocess
import uuid
from datetime import datetime
from pathlib import Path
import bpy
from .models import get_model, selected_model_key
DA3_COMMIT = "3d835ec1a5802d64a8b8b15f817a1ab54809bfe4"
RUNTIME_SCHEMA = 2
DEFAULT_STORAGE_ROOT = Path.home() / ".snapform"
def preferences():
addon = bpy.context.preferences.addons.get(__package__)
return addon.preferences if addon is not None else None
def storage_root():
addon_preferences = preferences()
configured = getattr(addon_preferences, "storage_root", "")
root = (
Path(bpy.path.abspath(configured)).expanduser()
if configured
else DEFAULT_STORAGE_ROOT
)
root.mkdir(parents=True, exist_ok=True)
return root
def runtime_directory():
return storage_root() / "runtime"
def runtime_python():
directory = runtime_directory()
if os.name == "nt":
return directory / "Scripts" / "python.exe"
return directory / "bin" / "python"
def runtime_ready():
ready_path = runtime_directory() / "snapform_ready.json"
if not runtime_python().is_file() or not ready_path.is_file():
return False
try:
ready = json.loads(ready_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return False
return ready.get("runtime_schema") == RUNTIME_SCHEMA
def models_directory():
path = storage_root() / "models"
path.mkdir(parents=True, exist_ok=True)
return path
def model_directory(model_key):
return models_directory() / model_key.lower()
def model_ready(model_key):
path = model_directory(model_key)
return (path / "config.json").is_file() and any(
path.glob("*.safetensors")
)
def jobs_directory():
path = storage_root() / "jobs"
path.mkdir(parents=True, exist_ok=True)
return path
def new_job_paths(job_type):
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
token = uuid.uuid4().hex[:8]
stem = f"{timestamp}-{job_type.lower()}-{token}"
directory = jobs_directory()
return {
"log": directory / f"{stem}.log",
"status": directory / f"{stem}.json",
"result": directory / stem,
}
def uv_executable():
addon_preferences = preferences()
configured = getattr(addon_preferences, "uv_executable", "uv")
resolved = shutil.which(configured)
if resolved:
return resolved
if Path(configured).is_file():
return str(Path(configured))
raise RuntimeError("uv executable was not found")
def install_runtime_command(status_path):
executable = uv_executable()
addon_preferences = preferences()
backend = getattr(addon_preferences, "torch_backend", "CU128")
script = Path(__file__).with_name("bootstrap_runtime.py")
return [
executable,
"run",
"--no-project",
"--python",
"3.12",
str(script),
"--uv",
executable,
"--runtime-dir",
str(runtime_directory()),
"--backend",
backend.lower(),
"--da3-commit",
DA3_COMMIT,
"--status",
str(status_path),
]
def download_model_command(model_key, status_path):
model = get_model(model_key)
return [
str(runtime_python()),
str(Path(__file__).with_name("worker.py")),
"download",
"--repository",
model.repository,
"--model-dir",
str(model_directory(model_key)),
"--status",
str(status_path),
]
def reconstruct_command(
settings,
export_format,
status_path,
result_directory,
input_path=None,
model_key=None,
process_res=None,
process_res_method=None,
use_ray_pose=None,
):
snapform_selected_key = model_key or selected_model_key(settings, export_format)
snapform_selected_process_res = (
settings.process_res if process_res is None else process_res
)
snapform_selected_process_res_method = (
settings.process_res_method
if process_res_method is None
else process_res_method
)
snapform_selected_ray_pose = (
settings.use_ray_pose if use_ray_pose is None else use_ray_pose
)
return [
str(runtime_python()),
str(Path(__file__).with_name("worker.py")),
"reconstruct",
"--input",
str(
Path(input_path)
if input_path is not None
else Path(bpy.path.abspath(settings.input_path))
),
"--model-dir",
str(model_directory(snapform_selected_key)),
"--output-dir",
str(result_directory),
"--export-format",
export_format,
"--status",
str(status_path),
"--process-res",
str(snapform_selected_process_res),
"--process-res-method",
snapform_selected_process_res_method,
"--conf-thresh-percentile",
str(settings.conf_thresh_percentile),
"--num-max-points",
str(settings.num_max_points),
"--show-cameras",
"1" if settings.show_cameras else "0",
"--gs-views-interval",
str(settings.gs_views_interval),
"--video-frames",
str(settings.video_frames),
"--device",
settings.device.lower(),
"--use-ray-pose",
"1" if snapform_selected_ray_pose else "0",
"--ref-view-strategy",
settings.ref_view_strategy,
]
def creation_flags():
if os.name != "nt":
return 0
return subprocess.CREATE_NO_WINDOW
def read_status(path):
try:
return json.loads(Path(path).read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None