render_view
Render a 3D model as PNG, SVG, or both to confirm its appearance visually, with control over direction, objects, quality, clipping, and camera angles.
Instructions
Render model. format: 'png' (raster, default), 'svg' (HLR line drawing — works without a display, no shading but precise edges), or 'both' (returns the PNG and SVG together — useful when you want shaded depth cues plus crisp edge geometry). If the raster path fails (typically headless host with no display backend) and format='png', the server falls back to SVG automatically. Renders confirm appearance, not geometry — verify boolean operations with measure() before rendering. direction: top, front, side, iso. objects: comma-separated names or name:color pairs e.g. 'u_frame:blue,roller:red' (default: all, auto-coloured). quality: standard, high. clip_plane: x, y, z to slice; clip_at: absolute world coordinate along that axis (default: each mesh's midpoint). azimuth/elevation: camera rotation in degrees applied after the direction preset. save_to: optional file path; for format='both' the PNG and SVG are written as <save_to>.png and <save_to>.svg.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| direction | No | iso | |
| objects | No | ||
| quality | No | standard | |
| clip_plane | No | ||
| clip_at | No | ||
| azimuth | No | ||
| elevation | No | ||
| save_to | No | ||
| format | No | png |
Implementation Reference
- src/build123d_mcp/server.py:21-51 (registration)MCP tool registration via @mcp.tool() decorator. The render_view function is registered as an MCP tool with its schema defined via type hints. It delegates to WorkerSession.render_view().
@mcp.tool() def render_view(direction: str = "iso", objects: str = "", quality: str = "standard", clip_plane: str = "", clip_at: float | None = None, azimuth: float = 0.0, elevation: float = 0.0, save_to: str = "", format: str = "png") -> list: """Render model. format: 'png' (raster, default), 'svg' (HLR line drawing — works without a display, no shading but precise edges), or 'both' (returns the PNG and SVG together — useful when you want shaded depth cues plus crisp edge geometry). If the raster path fails (typically headless host with no display backend) and format='png', the server falls back to SVG automatically. Renders confirm appearance, not geometry — verify boolean operations with measure() before rendering. direction: top, front, side, iso. objects: comma-separated names or name:color pairs e.g. 'u_frame:blue,roller:red' (default: all, auto-coloured). quality: standard, high. clip_plane: x, y, z to slice; clip_at: absolute world coordinate along that axis (default: each mesh's midpoint). azimuth/elevation: camera rotation in degrees applied after the direction preset. save_to: optional file path; for format='both' the PNG and SVG are written as <save_to>.png and <save_to>.svg.""" result = _session.render_view( direction=direction, objects=objects, quality=quality, clip_plane=clip_plane, clip_at=clip_at, azimuth=azimuth, elevation=elevation, save_to=save_to, format=format, ) contents: list = [] for key, suffix, mime in (("png", ".png", "image/png"), ("svg", ".svg", "image/svg+xml")): if key in result: data = result[key] fd, path = tempfile.mkstemp(suffix=suffix, prefix="build123d_") os.close(fd) with open(path, "wb") as f: f.write(data) contents.append(ImageContent( type="image", data=base64.b64encode(data).decode(), mimeType=mime, )) contents.append(TextContent(type="text", text=f"[SEND: {path}]")) if result.get("fallback"): contents.append(TextContent(type="text", text=result["fallback"])) if result.get("png_error"): contents.append(TextContent(type="text", text=f"PNG render failed: {result['png_error']}")) if result.get("png_warnings"): for w in result["png_warnings"]: contents.append(TextContent(type="text", text=f"Warning: {w}")) return contents - Core render_view handler function. Validates inputs, resolves shapes, renders PNG (VTK) with fallback to SVG (build123d HLR projection), and optionally saves to file. Returns dict with png/svg bytes.
def render_view( session, direction: str = "iso", objects: str = "", quality: str = "standard", clip_plane: str = "", clip_at: float | None = None, azimuth: float = 0.0, elevation: float = 0.0, save_to: str = "", format: str = "png", ) -> dict: """Render the active session geometry. Returns a dict with optional keys: - "png": bytes of the rasterised PNG (when format in ("png","both") or as automatic fallback failed) - "svg": bytes of the SVG document (when format in ("svg","both") or as automatic fallback when raster rendering failed) - "fallback": str, present when SVG was returned in place of a requested PNG because the VTK renderer failed """ direction = direction.lower() if direction not in ("top", "front", "side", "iso"): raise ValueError(f"Unknown direction '{direction}'. Use: top, front, side, iso") quality = quality.lower() if quality not in _QUALITY: raise ValueError(f"Unknown quality '{quality}'. Use: standard, high") clip_plane = clip_plane.lower() if clip_plane and clip_plane not in ("x", "y", "z"): raise ValueError(f"Unknown clip_plane '{clip_plane}'. Use: x, y, z") format = format.lower() if format not in _VALID_FORMATS: raise ValueError(f"Unknown format '{format}'. Use: png, svg, both") shapes = _resolve_shapes(session, objects) tess = _QUALITY[quality] result: dict = {} if format in ("png", "both"): try: png_bytes, png_failed = _do_render_png( shapes, tess, direction, clip_plane, clip_at, azimuth, elevation, ) result["png"] = png_bytes if png_failed: result["png_warnings"] = [ f"Skipped shapes (tessellation failed): {', '.join(png_failed)}" ] except Exception as exc: if format == "png": # Auto-fallback: produce SVG so the AI still gets a visual. result["svg"] = _do_render_svg( shapes, direction, clip_plane, clip_at, azimuth, elevation, ) result["format"] = "svg" result["fallback"] = ( f"VTK raster render failed ({type(exc).__name__}: {exc}). " f"Returning SVG via build123d HLR projection. " f"Common causes: no DISPLAY and no OSMesa/EGL backend on a headless host." ) else: # 'both' was requested. Record the PNG failure and continue to SVG. result["png_error"] = f"{type(exc).__name__}: {exc}" if format in ("svg", "both") and "svg" not in result: result["svg"] = _do_render_svg( shapes, direction, clip_plane, clip_at, azimuth, elevation, ) if save_to: from build123d_mcp.tools._paths import safe_output_path # Strip a known extension so format='both' produces consistent <base>.png and <base>.svg base, ext = os.path.splitext(save_to) if ext.lower() in (".png", ".svg"): save_to = base if "png" in result: with open(safe_output_path(save_to + ".png"), "wb") as f: f.write(result["png"]) if "svg" in result: with open(safe_output_path(save_to + ".svg"), "wb") as f: f.write(result["svg"]) return result - src/build123d_mcp/worker.py:220-246 (handler)WorkerSession.render_view() proxy method. Sends a 'render_view' operation over the multiprocessing pipe to the worker subprocess, which dispatches to the actual handler in render.py.
def render_view( self, direction: str = "iso", objects: str = "", quality: str = "standard", clip_plane: str = "", clip_at: float | None = None, azimuth: float = 0.0, elevation: float = 0.0, save_to: str = "", format: str = "png", ) -> dict: return self._call( "render_view", { "direction": direction, "objects": objects, "quality": quality, "clip_plane": clip_plane, "clip_at": clip_at, "azimuth": azimuth, "elevation": elevation, "save_to": save_to, "format": format, }, self._RENDER_TIMEOUT, ) - src/build123d_mcp/worker.py:58-64 (registration)Worker dispatch: routes the 'render_view' operation string to the render_view function from build123d_mcp.tools.render.
def _dispatch(session: Any, op: str, args: dict, library_index: Any) -> Any: if op == "execute": return session.execute(args["code"]) if op == "render_view": from build123d_mcp.tools.render import render_view return render_view(session, **args) - PNG rendering helper (_do_render_png): uses VTK offscreen rendering to rasterize shapes with tessellation, clipping, camera positioning, and PNG output via vtkPNGWriter.
def _do_render_png(shapes, tess, direction, clip_plane, clip_at, azimuth, elevation) -> tuple[bytes, list[str]]: import tempfile import vtk _ensure_display() renderer = vtk.vtkRenderer() renderer.SetBackground(1.0, 1.0, 1.0) render_window = vtk.vtkRenderWindow() render_window.SetOffScreenRendering(1) render_window.SetSize(800, 600) render_window.AddRenderer(renderer) failed: list[str] = [] actor_count = 0 for i, (name, shape, obj_color) in enumerate(shapes): try: verts, tris = shape.tessellate( tess["linear_deflection"], tess["angular_deflection"] ) except Exception as exc: failed.append(f"{name}: {exc}") continue points = vtk.vtkPoints() for v in verts: points.InsertNextPoint(v.X, v.Y, v.Z) cells = vtk.vtkCellArray() for tri in tris: cells.InsertNextCell(3) cells.InsertCellPoint(tri[0]) cells.InsertCellPoint(tri[1]) cells.InsertCellPoint(tri[2]) poly = vtk.vtkPolyData() poly.SetPoints(points) poly.SetPolys(cells) if clip_plane: if clip_at is not None: origin = {"x": (clip_at, 0, 0), "y": (0, clip_at, 0), "z": (0, 0, clip_at)}[clip_plane] else: bounds = poly.GetBounds() # xmin, xmax, ymin, ymax, zmin, zmax cx = (bounds[0] + bounds[1]) / 2 cy = (bounds[2] + bounds[3]) / 2 cz = (bounds[4] + bounds[5]) / 2 origin = {"x": (cx, 0, 0), "y": (0, cy, 0), "z": (0, 0, cz)}[clip_plane] normal = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}[clip_plane] plane = vtk.vtkPlane() plane.SetOrigin(*origin) plane.SetNormal(*normal) clipper = vtk.vtkClipPolyData() clipper.SetInputData(poly) clipper.SetClipFunction(plane) clipper.SetInsideOut(False) clipper.Update() poly = clipper.GetOutput() mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(poly) actor = vtk.vtkActor() actor.SetMapper(mapper) r, g, b = _color_to_rgb(obj_color if obj_color else _PALETTE[i % len(_PALETTE)]) prop = actor.GetProperty() prop.SetColor(r, g, b) prop.SetAmbient(0.3) prop.SetDiffuse(0.7) prop.SetSpecular(0.2) prop.SetInterpolationToPhong() # smooth shading renderer.AddActor(actor) actor_count += 1 if actor_count == 0: msg = "All shapes failed to tessellate: " + "; ".join(failed) if failed else "No geometry to render" raise RuntimeError(msg) # Camera setup camera = renderer.GetActiveCamera() camera.SetParallelProjection(False) pos, up = _camera_direction(direction) camera.SetPosition(*pos) camera.SetFocalPoint(0.0, 0.0, 0.0) camera.SetViewUp(*up) renderer.ResetCamera() if azimuth != 0.0 or elevation != 0.0: camera.Azimuth(azimuth) camera.Elevation(elevation) camera.OrthogonalizeViewUp() renderer.ResetCameraClippingRange() render_window.Render() w2i = vtk.vtkWindowToImageFilter() w2i.SetInput(render_window) w2i.Update() with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: png_path = os.path.join(tmpdir, "render.png") writer = vtk.vtkPNGWriter() writer.SetFileName(png_path) writer.SetInputConnection(w2i.GetOutputPort()) writer.Write() with open(png_path, "rb") as f: return f.read(), failed