$ cat node-template.py
3
3D Print Check
// Analyzes a 3D model for printability without modifying it: watertightness, self-intersections, thin walls, overhangs, stability and size metrics. Outputs a scored report, a pass/fail verdict and a preview with problem areas highlighted.
Process
3D
#3d#mesh#3d-print#validation#analysis
template.py
1"""print-check-3d — analyze a 3D model for printability (read-only).23Calls Gais.model.print_check: watertightness, self-intersections, thin4walls, overhangs, stability, size metrics -> scored report + optional5color-coded preview. The input model is never modified. No os/pathlib/HTTP6here (lint-blocked).7"""89from __future__ import annotations1011import json12import sys13import traceback14from typing import Any1516from gais import Gais1718INPUT_DIR = "/data/input"19OUTPUT_DIR = "/data/output"2021PREVIEW_FILENAME = "check_preview.glb"2223SUPPORTED_EXTENSIONS = (".glb", ".gltf", ".stl", ".obj")242526def _f(value: Any, default: float) -> float:27 try:28 return float(value)29 except (TypeError, ValueError):30 return default313233def _i(value: Any, default: int) -> int:34 try:35 return int(float(value))36 except (TypeError, ValueError):37 return default383940def _b(value: Any, default: bool) -> bool:41 if isinstance(value, bool):42 return value43 if isinstance(value, str):44 return value.strip().lower() in ("1", "true", "yes", "on")45 if value is None:46 return default47 return bool(value)484950def _input_file_path(inputs: dict[str, Any], key: str) -> str | None:51 name = inputs.get(key)52 if not name or not isinstance(name, str):53 return None54 base = name.replace("\\", "/").rsplit("/", 1)[-1].strip()55 if not base or base in (".", ".."):56 return None57 return f"{INPUT_DIR}/{base}"585960def _copy_to_output(src_path: str, name: str) -> None:61 with open(src_path, "rb") as src, open(f"{OUTPUT_DIR}/{name}", "wb") as dst:62 while True:63 chunk = src.read(1024 * 1024)64 if not chunk:65 break66 dst.write(chunk)676869def process(inputs: dict[str, Any]) -> dict[str, Any]:70 mesh_path = _input_file_path(inputs, "mesh")71 if mesh_path is None:72 raise ValueError("Required input 'mesh' not provided")73 if not mesh_path.lower().endswith(SUPPORTED_EXTENSIONS):74 raise ValueError(75 f"Unsupported mesh format ({mesh_path.rsplit('/', 1)[-1]}). "76 f"Supported: {', '.join(SUPPORTED_EXTENSIONS)}."77 )7879 annotate = _b(inputs.get("annotate"), True)80 result = Gais.model.print_check(81 mesh_path,82 target_size_mm=_f(inputs.get("target_size_mm"), 100.0),83 min_wall_thickness_mm=_f(inputs.get("min_wall_thickness_mm"), 1.5),84 overhang_angle_deg=_f(inputs.get("overhang_angle_deg"), 45.0),85 annotate=annotate,86 wall_samples_per_face=_i(inputs.get("wall_samples_per_face"), 3),87 )8889 with open(str(result.file("report")), "r", encoding="utf-8") as fh:90 report = json.load(fh)91 summary = report.get("summary", {})9293 outputs: dict[str, Any] = {94 "report": json.dumps(report),95 "printable": bool(summary.get("printable", False)),96 "score": _i(summary.get("score"), 0),97 }9899 if annotate:100 _copy_to_output(str(result.file("preview")), PREVIEW_FILENAME)101 outputs["preview"] = PREVIEW_FILENAME102103 issues = report.get("issues", [])104 if issues:105 print(106 f"[print-check-3d] {len(issues)} issue(s), score={outputs['score']}, "107 f"printable={outputs['printable']}",108 file=sys.stderr,109 )110111 return outputs112113114def main() -> None:115 try:116 envelope = json.loads(sys.stdin.read() or "{}")117 if not isinstance(envelope, dict):118 envelope = {}119 inputs = envelope.get("inputs", {}) or {}120 json.dump(process(inputs), sys.stdout)121 except Exception as e:122 print(123 json.dumps({124 "error": str(e),125 "errorType": type(e).__name__,126 "traceback": traceback.format_exc(),127 }),128 file=sys.stderr,129 )130 sys.exit(1)131132133if __name__ == "__main__":134 main()$ git log --oneline
v1.0.0
HEAD
2026-08-18