"""Deterministic GPU-free isometric renderer for FreeCAD tessellation output.""" from __future__ import annotations import json import math import os from pathlib import Path MAX_TRIANGLES = 500_000 def render( mesh_path: Path, output_path: Path, options: dict[str, object] | None = None ) -> None: from PIL import Image, ImageColor, ImageDraw payload = json.loads(mesh_path.read_text(encoding="utf-8")) vertices = payload.get("vertices") triangles = payload.get("triangles") if not isinstance(vertices, list) or not isinstance(triangles, list): raise TypeError("FREECAD_PREVIEW_MESH_INVALID") if not vertices or not triangles or len(triangles) > MAX_TRIANGLES: raise RuntimeError("FREECAD_PREVIEW_LIMIT_EXCEEDED") settings = options or {} width = int(settings.get("width", 1200)) height = int(settings.get("height", 900)) if not 320 <= width <= 1600 or not 240 <= height <= 1600: raise RuntimeError("FREECAD_PREVIEW_SIZE_INVALID") background = ImageColor.getrgb(str(settings.get("background", "#F7F9FC"))) projected: list[tuple[float, float, float]] = [] for item in vertices: if not isinstance(item, list) or len(item) != 3: raise RuntimeError("FREECAD_PREVIEW_MESH_INVALID") x, y, z = map(float, item) projected.append( (0.8660254 * (x - y), 0.5 * (x + y) - z, 0.4082483 * (x + y + z)) ) xs = [item[0] for item in projected] ys = [item[1] for item in projected] span_x = max(xs) - min(xs) span_y = max(ys) - min(ys) scale = min(width * 0.82 / max(span_x, 1e-12), height * 0.82 / max(span_y, 1e-12)) center_x = (min(xs) + max(xs)) / 2 center_y = (min(ys) + max(ys)) / 2 points = [ ((x - center_x) * scale + width / 2, (y - center_y) * scale + height / 2, depth) for x, y, depth in projected ] image = Image.new("RGB", (width, height), background) draw = ImageDraw.Draw(image) light = (0.3, -0.45, 0.84) ordered = sorted( triangles, key=lambda tri: sum(points[int(index)][2] for index in tri) / 3 ) for triangle in ordered: if not isinstance(triangle, list) or len(triangle) != 3: raise RuntimeError("FREECAD_PREVIEW_MESH_INVALID") indices = [int(item) for item in triangle] try: a, b, c = (vertices[index] for index in indices) except (IndexError, TypeError) as exc: raise RuntimeError("FREECAD_PREVIEW_MESH_INVALID") from exc ab = tuple(float(b[i]) - float(a[i]) for i in range(3)) ac = tuple(float(c[i]) - float(a[i]) for i in range(3)) normal = ( ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0], ) magnitude = max(math.sqrt(sum(item * item for item in normal)), 1e-12) shade = 0.42 + 0.48 * abs( sum(normal[i] * light[i] for i in range(3)) / magnitude ) color = tuple(int(channel * shade) for channel in (96, 158, 210)) draw.polygon( [(points[index][0], points[index][1]) for index in indices], fill=color, outline=(45, 65, 82), ) temporary = output_path.with_name(output_path.name + ".tmp.png") image.save(temporary, format="PNG", optimize=True) os.replace(temporary, output_path)