$ cat node-template.py
3
3D Print Prep
// Prepares a 3D model for printing: cleanup, watertight repair, gap sealing, optional remesh/hollowing/decimation. Outputs a printable STL (mm), a preview and a validation report.
Process
3D
#3d#mesh#3d-print#stl#watertight#repair
template.py
1"""print-prep-3d — prepare a 3D model for printing via headless Blender.23Calls Gais.model.prepare_print (gais-blender service): cleanup ->4make-manifold repair -> Manifold-solver boolean sealing -> optional5remesh/hollow/decimate -> validation. Three outputs: printable STL (mm),6GLB preview, JSON validation report. No os/pathlib/HTTP here7(lint-blocked); files are stream-copied with plain open().8"""910from __future__ import annotations1112import json13import sys14import traceback15from typing import Any1617from gais import Gais1819INPUT_DIR = "/data/input"20OUTPUT_DIR = "/data/output"2122STL_FILENAME = "print_ready.stl"23PREVIEW_FILENAME = "print_preview.glb"2425SUPPORTED_EXTENSIONS = (".glb", ".gltf", ".stl", ".obj")262728def _f(value: Any, default: float) -> float:29 try:30 return float(value)31 except (TypeError, ValueError):32 return default333435def _i(value: Any, default: int) -> int:36 try:37 return int(float(value))38 except (TypeError, ValueError):39 return default404142def _b(value: Any, default: bool) -> bool:43 if isinstance(value, bool):44 return value45 if isinstance(value, str):46 return value.strip().lower() in ("1", "true", "yes", "on")47 if value is None:48 return default49 return bool(value)505152def _input_file_path(inputs: dict[str, Any], key: str) -> str | None:53 """Upstream file ports deliver a bare filename under /data/input."""54 name = inputs.get(key)55 if not name or not isinstance(name, str):56 return None57 base = name.replace("\\", "/").rsplit("/", 1)[-1].strip()58 if not base or base in (".", ".."):59 return None60 return f"{INPUT_DIR}/{base}"616263def _copy_to_output(src_path: str, name: str) -> None:64 """Stream-copy in 1 MiB chunks (large STLs must not be slurped)."""65 with open(src_path, "rb") as src, open(f"{OUTPUT_DIR}/{name}", "wb") as dst:66 while True:67 chunk = src.read(1024 * 1024)68 if not chunk:69 break70 dst.write(chunk)717273def process(inputs: dict[str, Any]) -> dict[str, Any]:74 mesh_path = _input_file_path(inputs, "mesh")75 if mesh_path is None:76 raise ValueError("Required input 'mesh' not provided")77 if not mesh_path.lower().endswith(SUPPORTED_EXTENSIONS):78 raise ValueError(79 f"Unsupported mesh format ({mesh_path.rsplit('/', 1)[-1]}). "80 f"Supported: {', '.join(SUPPORTED_EXTENSIONS)}."81 )8283 remesh_method = str(inputs.get("remesh_method") or "voxel")84 if remesh_method not in ("none", "voxel", "quadriflow"):85 raise ValueError(f"Unknown remesh_method {remesh_method!r}")8687 result = Gais.model.prepare_print(88 mesh_path,89 target_size_mm=_f(inputs.get("target_size_mm"), 100.0),90 wall_thickness_mm=_f(inputs.get("wall_thickness_mm"), 0.0),91 remesh_method=remesh_method,92 voxel_size_mm=_f(inputs.get("voxel_size_mm"), 0.5),93 max_faces=_i(inputs.get("max_faces"), 500000),94 fail_on_issues=_b(inputs.get("fail_on_issues"), False),95 merge_distance_mm=_f(inputs.get("merge_distance_mm"), 0.01),96 target_faces=_i(inputs.get("target_faces"), 5000),97 )9899 _copy_to_output(str(result.file("model_stl")), STL_FILENAME)100 _copy_to_output(str(result.file("preview")), PREVIEW_FILENAME)101102 with open(str(result.file("report")), "r", encoding="utf-8") as fh:103 report = json.load(fh)104105 if not report.get("is_watertight", False):106 print(107 f"[print-prep-3d] warning: result is not watertight "108 f"({report.get('non_manifold_edges')} non-manifold edges)",109 file=sys.stderr,110 )111112 return {113 "model_stl": STL_FILENAME,114 "preview": PREVIEW_FILENAME,115 "report": json.dumps(report),116 }117118119def main() -> None:120 try:121 envelope = json.loads(sys.stdin.read() or "{}")122 if not isinstance(envelope, dict):123 envelope = {}124 inputs = envelope.get("inputs", {}) or {}125 json.dump(process(inputs), sys.stdout)126 except Exception as e:127 print(128 json.dumps({129 "error": str(e),130 "errorType": type(e).__name__,131 "traceback": traceback.format_exc(),132 }),133 file=sys.stderr,134 )135 sys.exit(1)136137138if __name__ == "__main__":139 main()$ git log --oneline
v1.2.0
HEAD
2026-08-18