| 1727 | } |
| 1728 | |
| 1729 | void register_rendering(nb::module_& m) { |
| 1730 | nb::class_<PyViewInfo>(m, "ViewInfo") |
| 1731 | .def_ro("rotation", &PyViewInfo::rotation) |
| 1732 | .def_ro("translation", &PyViewInfo::translation) |
| 1733 | .def_ro("width", &PyViewInfo::width) |
| 1734 | .def_ro("height", &PyViewInfo::height) |
| 1735 | .def_ro("fov_x", &PyViewInfo::fov_x) |
| 1736 | .def_ro("fov_y", &PyViewInfo::fov_y) |
| 1737 | .def_ro("orthographic", &PyViewInfo::orthographic) |
| 1738 | .def_ro("ortho_scale", &PyViewInfo::ortho_scale) |
| 1739 | .def_prop_ro( |
| 1740 | "ortho_view_extent_world", [](const PyViewInfo& self) -> float { |
| 1741 | if (!self.orthographic || self.ortho_scale <= 0.0f) |
| 1742 | return 0.0f; |
| 1743 | return static_cast<float>(self.height) / self.ortho_scale; |
| 1744 | }, |
| 1745 | "Vertical view extent in world units (Blender-compatible orthographic scale). Larger when zoomed out, smaller when zoomed in.") |
| 1746 | .def_prop_ro("position", [](const PyViewInfo& self) -> std::tuple<float, float, float> { |
| 1747 | auto t = self.translation.tensor().cpu(); |
| 1748 | auto acc = t.accessor<float, 1>(); |
| 1749 | return {acc(0), acc(1), acc(2)}; }, "Camera position as (x, y, z) tuple"); |
| 1750 | |
| 1751 | nb::class_<PyViewportRender>(m, "ViewportRender") |
| 1752 | .def_ro("image", &PyViewportRender::image) |
| 1753 | .def_ro("screen_positions", &PyViewportRender::screen_positions); |
| 1754 | |
| 1755 | m.def("get_viewport_render", &get_viewport_render, |
| 1756 | "Get the most recently captured CPU-visible viewport render if available (does not force GPU readback)"); |
| 1757 | |
| 1758 | m.def("capture_viewport", &capture_viewport, |
| 1759 | "Capture viewport render explicitly (may read back from GPU; clones data, safe to use from background threads)"); |
| 1760 | |
| 1761 | m.def("export_viewport_image", |
| 1762 | &export_viewport_image, |
| 1763 | nb::arg("path"), |
| 1764 | nb::arg("format") = std::string{}, |
| 1765 | nb::arg("width") = 0, |
| 1766 | nb::arg("height") = 0, |
| 1767 | nb::arg("transparent") = false, |
| 1768 | nb::arg("jpeg_quality") = 95, |
| 1769 | R"doc( |
| 1770 | Export the active viewport image to PNG or JPEG. |
| 1771 | |
| 1772 | Args: |
| 1773 | path: Output path. The selected format extension is enforced when needed. |
| 1774 | format: 'png', 'jpg', or empty to infer from path. |
| 1775 | width: Target width in pixels. If zero with a positive height, preserves viewport aspect. |
| 1776 | height: Target height in pixels. If both dimensions are zero, captures the current viewport. |
| 1777 | transparent: For PNG only, export straight RGBA from the preview renderer. |
| 1778 | jpeg_quality: JPEG compression quality in [1, 100]. |
| 1779 | |
| 1780 | Returns: |
| 1781 | Dict with path, width, height, channels, format, and transparent. |
| 1782 | )doc"); |
| 1783 | |
| 1784 | m.def( |
| 1785 | "render_view", |
| 1786 | [](const PyTensor& rotation, const PyTensor& translation, int width, int height, |
no test coverage detected