$ 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