$ cat node-template.py
3
3D Snapshot
// Compose a view of a 3D model directly on the node card — orbit, zoom, field of view, environment and material settings — then capture it as an image at a configurable aspect ratio and resolution. The captured picture is exactly what the preview shows.
Process
3D
#3d#mesh#snapshot#render#image
template.py
1"""snapshot-3d — persist a snapshot composed in the node card's 3D preview.23The Capture button on the node card renders the preview offscreen at the4chosen aspect ratio/resolution and stores the pixels in the `snapshot`5input (data URI) together with the composed `camera` state. This node6simply decodes those pixels into the `image` output — what you saw is7what you get. No os/pathlib/HTTP here (lint-blocked).8"""910from __future__ import annotations1112import base6413import json14import sys15import traceback16from typing import Any1718INPUT_DIR = "/data/input"19OUTPUT_DIR = "/data/output"2021SUPPORTED_EXTENSIONS = (".glb", ".gltf")2223WEBP_MAGIC = b"RIFF"24PNG_MAGIC = b"\x89PNG\r\n\x1a\n"2526MIN_LONG_EDGE = 25627MAX_LONG_EDGE = 2048282930def _i(value: Any, default: int) -> int:31 try:32 return int(float(value))33 except (TypeError, ValueError):34 return default353637def _input_file_path(inputs: dict[str, Any], key: str) -> str | None:38 name = inputs.get(key)39 if not name or not isinstance(name, str):40 return None41 base = name.replace("\\", "/").rsplit("/", 1)[-1].strip()42 if not base or base in (".", ".."):43 return None44 return f"{INPUT_DIR}/{base}"454647def _dims(ratio: Any, resolution: Any) -> tuple[int, int]:48 """Long edge = resolution, short edge scaled by the W:H ratio, even ints.4950 Must stay in sync with dimsFromRatio in the snapshot composer widget.51 """52 try:53 w_part, h_part = str(ratio or "1:1").split(":", 1)54 rw, rh = float(w_part), float(h_part)55 if rw <= 0 or rh <= 0:56 raise ValueError57 except (TypeError, ValueError):58 rw, rh = 1.0, 1.059 long_edge = min(max(_i(resolution, 1024), MIN_LONG_EDGE), MAX_LONG_EDGE)60 if rw >= rh:61 w, h = long_edge, long_edge * rh / rw62 else:63 w, h = long_edge * rw / rh, long_edge64 even = lambda v: max(2, int(round(v / 2)) * 2) # noqa: E73165 return even(w), even(h)666768def _parse_camera(raw: Any) -> dict[str, Any]:69 if isinstance(raw, dict):70 return raw71 if isinstance(raw, str) and raw.strip():72 try:73 parsed = json.loads(raw)74 if isinstance(parsed, dict):75 return parsed76 except ValueError:77 pass78 return {}798081def _decode_snapshot(raw: str) -> bytes:82 payload = raw.strip()83 if payload.startswith("data:"):84 header, _, payload = payload.partition(",")85 if not header.startswith("data:image/") or not payload:86 raise ValueError(87 "The captured snapshot is not an image data URI. "88 "Press Capture on the node card to take a new one."89 )90 try:91 return base64.b64decode(payload, validate=True)92 except (ValueError, TypeError) as e:93 raise ValueError(94 "The captured snapshot could not be decoded. "95 "Press Capture on the node card to take a new one."96 ) from e979899def process(inputs: dict[str, Any]) -> dict[str, Any]:100 mesh_path = _input_file_path(inputs, "mesh")101 if mesh_path is None:102 raise ValueError("Required input 'mesh' not provided")103 if not mesh_path.lower().endswith(SUPPORTED_EXTENSIONS):104 raise ValueError(105 f"Unsupported mesh format ({mesh_path.rsplit('/', 1)[-1]}). "106 f"The node preview supports: {', '.join(SUPPORTED_EXTENSIONS)}."107 )108109 snapshot = inputs.get("snapshot")110 if not snapshot or not isinstance(snapshot, str) or not snapshot.strip():111 raise ValueError(112 "No snapshot captured yet — connect a 3D model and press "113 "Capture on the node card."114 )115116 raw = _decode_snapshot(snapshot)117 if raw.startswith(WEBP_MAGIC):118 ext = "webp"119 elif raw.startswith(PNG_MAGIC):120 ext = "png"121 else:122 raise ValueError(123 "The captured snapshot is not a valid PNG or WebP image. "124 "Press Capture on the node card to take a new one."125 )126127 filename = f"snapshot.{ext}"128 with open(f"{OUTPUT_DIR}/{filename}", "wb") as fh:129 fh.write(raw)130131 width, height = _dims(inputs.get("ratio"), inputs.get("resolution"))132 camera = _parse_camera(inputs.get("camera"))133 print(134 f"[snapshot-3d] wrote {filename} ({len(raw)} bytes, target {width}x{height}, "135 f"fov={camera.get('fov', 'default')})",136 file=sys.stderr,137 )138139 return {"image": filename}140141142def main() -> None:143 try:144 envelope = json.loads(sys.stdin.read() or "{}")145 if not isinstance(envelope, dict):146 envelope = {}147 inputs = envelope.get("inputs", {}) or {}148 json.dump(process(inputs), sys.stdout)149 except Exception as e:150 print(151 json.dumps({152 "error": str(e),153 "errorType": type(e).__name__,154 "traceback": traceback.format_exc(),155 }),156 file=sys.stderr,157 )158 sys.exit(1)159160161if __name__ == "__main__":162 main()$ git log --oneline
v1.0.0
HEAD
2026-08-18