import os from typing import Dict, Optional import yaml def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str: """Create an SVG preview by placing real public _BB.gds cells from layout YAML.""" layout = yaml.safe_load(yaml_content) or {} try: return _create_with_gdstk(layout, output_path, pdk_registry, project_dir) except ImportError as gdstk_error: try: return _create_with_nazca(layout, output_path, pdk_registry, project_dir) except ImportError as nazca_error: raise RuntimeError( "Layout SVG requires GDS geometry support. Install gdstk, or fix nazca dependencies. " f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}" ) from nazca_error def _create_with_gdstk(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str: import gdstk library = gdstk.Library() cell_cache = {} top = _build_gdstk_cell(gdstk, library, layout, pdk_registry, project_dir, cell_cache) os.makedirs(os.path.dirname(output_path), exist_ok=True) top.write_svg(output_path) return output_path def _build_gdstk_cell(gdstk, library, layout: dict, pdk_registry, project_dir: Optional[str], cell_cache: Dict): cell_name = _safe_cell_name(layout.get("name") or "layout", library) top = library.new_cell(cell_name) for instance_name, instance in (layout.get("instances") or {}).items(): component = str(instance.get("component") or "") x = _number(instance.get("x")) y = _number(instance.get("y")) rotation = _number(instance.get("rotation")) * 3.141592653589793 / 180 child = _resolve_child_cell(gdstk, library, component, pdk_registry, project_dir, cell_cache) if child is None: raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}") top.add(gdstk.Reference(child, origin=(x, y), rotation=rotation)) return top def _resolve_child_cell(gdstk, library, component: str, pdk_registry, project_dir: Optional[str], cell_cache: Dict): if component in cell_cache: return cell_cache[component] local_layout = _load_local_layout(component, project_dir) if local_layout is not None: child = _build_gdstk_cell(gdstk, library, local_layout, pdk_registry, project_dir, cell_cache) cell_cache[component] = child return child asset = pdk_registry.resolve(component) if not asset.gds_path: return None child = _import_gds_cell(gdstk, library, asset.gds_path) cell_cache[component] = child return child def _import_gds_cell(gdstk, library, gds_path: str): source = gdstk.read_gds(gds_path) top_cells = source.top_level() if not top_cells: raise ValueError(f"No top-level cell found in {gds_path}") for source_cell in source.cells: if _library_cell_by_name(library, source_cell.name) is None: library.add(source_cell) return top_cells[0] def _create_with_nazca(layout: dict, output_path: str, pdk_registry, project_dir: Optional[str]) -> str: import nazca as nd png_path = os.path.splitext(output_path)[0] + ".gds" with nd.Cell(str(layout.get("name") or "layout")) as top: for instance_name, instance in (layout.get("instances") or {}).items(): component = str(instance.get("component") or "") asset = pdk_registry.resolve(component) if not asset.gds_path: raise FileNotFoundError(f"Unable to resolve _BB.gds for instance {instance_name}: {component}") loaded = nd.load_gds(asset.gds_path) loaded.put(_number(instance.get("x")), _number(instance.get("y")), _number(instance.get("rotation"))) nd.export_gds(top, filename=png_path) raise RuntimeError( "Nazca can build the placed GDS, but SVG preview export requires gdstk in this backend." ) def _load_local_layout(component: str, project_dir: Optional[str]) -> Optional[dict]: if not project_dir or "/" in component or "\\" in component or component == "generate_with_forge": return None for ext in (".yml", ".yaml"): path = os.path.join(project_dir, f"{component}{ext}") if os.path.exists(path): with open(path, "r", encoding="utf-8") as file: return yaml.safe_load(file) or {} return None def _safe_cell_name(name, library) -> str: base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "layout" candidate = base counter = 1 while _library_cell_by_name(library, candidate) is not None: counter += 1 candidate = f"{base}_{counter}" return candidate def _library_cell_by_name(library, name: str): for cell in library.cells: if cell.name == name: return cell return None def _number(value, default=0.0) -> float: try: if value is None or value == "": return default return float(value) except (TypeError, ValueError): return default