""" Render a cross-section as SVG. WHY SEPARATELY CLASSED PARTS ROADMAP section 4 gives this as the reason the port exists at all: the outline, each stock cavity and the stock itself are emitted as distinct classed groups, so a viewer can highlight or hide them independently. A single flattened silhouette would have been far easier and would not have justified leaving OpenSCAD. COORDINATES Geometry has +Y up; SVG has +Y down. The whole drawing is flipped once in a wrapping transform rather than by negating coordinates, so the paths in the file remain the geometry's own numbers and can be read against the report. HOLES A region is a list of rings, some of which are holes, and their winding direction is not something this module wants to depend on. Every ring of a region goes into one path with ``fill-rule="evenodd"``, which renders holes correctly whichever way they wind. """ from __future__ import annotations from typing import Iterable, List, Optional, Sequence, Tuple Point = Tuple[float, float] Path = List[Point] Region = Sequence[Path] def _fmt(v: float) -> str: """Trim to 4 decimal places -- well below anything visible at any zoom.""" return ("%.4f" % v).rstrip("0").rstrip(".") or "0" def path_d(region: Region) -> str: """One SVG path definition covering every ring of a region.""" parts: List[str] = [] for ring in region: if len(ring) < 3: continue parts.append("M " + " L ".join("%s %s" % (_fmt(x), _fmt(y)) for x, y in ring) + " Z") return " ".join(parts) def bounds(regions: Iterable[Region]) -> Tuple[float, float, float, float]: xs: List[float] = [] ys: List[float] = [] for region in regions: for ring in region: for x, y in ring: xs.append(x) ys.append(y) if not xs: return (-1.0, -1.0, 1.0, 1.0) return (min(xs), min(ys), max(xs), max(ys)) def render(section: Region, cavities: Optional[Region] = None, stock: Optional[Sequence[Path]] = None, margin: float = 3.0, width_px: int = 520) -> str: """ The cross-section as a standalone SVG document. ``section`` is the printed material. ``cavities`` are the voids the stock passes through. ``stock`` is the stock itself, drawn inside its cavity so the fit gap is visible -- which is the whole point of looking at it. """ regions: List[Region] = [section] if cavities: regions.append(cavities) if stock: regions.append(list(stock)) x0, y0, x1, y1 = bounds(regions) x0 -= margin y0 -= margin x1 += margin y1 += margin w = x1 - x0 h = y1 - y0 height_px = int(round(width_px * (h / w))) if w > 0 else width_px out: List[str] = [ '' % (_fmt(x0), _fmt(y0), _fmt(w), _fmt(h), width_px, height_px), '', # One flip for the whole drawing. Everything below is in geometry # coordinates, so a path here can be read against the report directly. '' % _fmt(y0 + y1), ] out.append('' '' % path_d(section)) if cavities: out.append('' '' % path_d(cavities)) if stock: out.append('' '' % path_d(list(stock))) out.append('') out.append('') return "\n".join(out) def render_result(result, geo=None) -> str: """ Render whatever ``build()`` returned. Cavities and stock are drawn when the geometry layer can supply them, and quietly omitted when it cannot -- a picture of the material alone is still useful, and a viewer should not fail because a helper moved. """ section = result.section cavities = None stock = None g = geo if geo is not None else getattr(result, "geo", None) members = getattr(result, "members", None) if g is not None and members: try: from .geom.join import cavity_region cavities = cavity_region(members, g) except Exception: # noqa: BLE001 cavities = None try: from .geom.records import strap_path stock = [strap_path(m, g) for m in members] except Exception: # noqa: BLE001 stock = None return render(section, cavities, stock)