Fix project reopen persistence
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Description: Layout YAML file filtering, parsing, and staging helpers.
|
||||
# Inside functions: is_layout_cell_filename, parse_layout_cell_content, read_layout_cell_file, load_layout_cell_files, layout_cell_filename, write_layout_cells_to_directory
|
||||
# Developer : Qin Yue @ 2026
|
||||
# Organization : OptiHK Limited
|
||||
# -----------------------------------------------------------------------------
|
||||
import os
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
LAYOUT_YAML_EXTENSIONS = (".yml", ".yaml")
|
||||
ROUTE_SIDECAR_SUFFIXES = (".routes.yml", ".routes.yaml")
|
||||
|
||||
|
||||
class LayoutFileError(ValueError):
|
||||
"""Raised when a saved layout YAML file cannot be used as a cell."""
|
||||
|
||||
|
||||
def is_layout_cell_filename(filename):
|
||||
"""Return True for user layout cell YAML files, excluding sidecars/manifests."""
|
||||
lower = (filename or "").lower()
|
||||
if lower == "technology.yml":
|
||||
return False
|
||||
if lower.endswith(ROUTE_SIDECAR_SUFFIXES):
|
||||
return False
|
||||
return lower.endswith(LAYOUT_YAML_EXTENSIONS)
|
||||
|
||||
|
||||
def is_layout_cell_document(data):
|
||||
"""Return True when parsed YAML looks like an mxPIC saved cell/project."""
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
|
||||
kind = data.get("kind")
|
||||
if kind and str(kind).strip().lower() != "cell":
|
||||
return False
|
||||
|
||||
layout_keys = {
|
||||
"schema_version",
|
||||
"canvas_size",
|
||||
"canvasSize",
|
||||
"instances",
|
||||
"elements",
|
||||
"pins",
|
||||
"ports",
|
||||
"bundles",
|
||||
}
|
||||
return any(key in data for key in layout_keys)
|
||||
|
||||
|
||||
def parse_layout_cell_content(content, source="layout YAML"):
|
||||
"""Parse and validate saved layout YAML content."""
|
||||
try:
|
||||
data = yaml.safe_load(content or "") or {}
|
||||
except yaml.YAMLError as exc:
|
||||
raise LayoutFileError(f"{source} is not valid YAML: {exc}") from exc
|
||||
|
||||
if not is_layout_cell_document(data):
|
||||
raise LayoutFileError(f"{source} is not a saved mxPIC layout cell")
|
||||
return data
|
||||
|
||||
|
||||
def read_layout_cell_file(path):
|
||||
"""Read a saved layout cell file and return parsed data plus raw content."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
except OSError as exc:
|
||||
raise LayoutFileError(f"{os.path.basename(path)} could not be read: {exc}") from exc
|
||||
return parse_layout_cell_content(content, os.path.basename(path)), content
|
||||
|
||||
|
||||
def load_layout_cell_files(project_dir):
|
||||
"""Load valid layout cell files from a project directory and collect warnings."""
|
||||
cells = []
|
||||
warnings = []
|
||||
if not os.path.isdir(project_dir):
|
||||
return cells, warnings
|
||||
|
||||
for filename in sorted(os.listdir(project_dir)):
|
||||
if not is_layout_cell_filename(filename):
|
||||
continue
|
||||
path = os.path.join(project_dir, filename)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
try:
|
||||
data, content = read_layout_cell_file(path)
|
||||
except LayoutFileError as exc:
|
||||
warnings.append(f"Skipped {filename}: {exc}")
|
||||
continue
|
||||
|
||||
cells.append({
|
||||
"filename": filename,
|
||||
"name": str(data.get("name") or os.path.splitext(filename)[0]),
|
||||
"data": data,
|
||||
"content": content,
|
||||
})
|
||||
return cells, warnings
|
||||
|
||||
|
||||
def layout_cell_filename(cell_name, fallback="canvas_1.yml"):
|
||||
"""Build a safe filename for staging a parsed layout cell."""
|
||||
name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(cell_name or "")).strip("._")
|
||||
if not name:
|
||||
return fallback
|
||||
return f"{name}.yml"
|
||||
|
||||
|
||||
def write_layout_cells_to_directory(cells, output_dir):
|
||||
"""Write valid layout cell contents into a clean staging directory."""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
used_names = set()
|
||||
for index, cell in enumerate(cells, start=1):
|
||||
filename = layout_cell_filename(cell.get("name"), cell.get("filename") or f"cell_{index}.yml")
|
||||
base, ext = os.path.splitext(filename)
|
||||
unique_filename = filename
|
||||
counter = 1
|
||||
while unique_filename.lower() in used_names:
|
||||
counter += 1
|
||||
unique_filename = f"{base}_{counter}{ext}"
|
||||
used_names.add(unique_filename.lower())
|
||||
|
||||
with open(os.path.join(output_dir, unique_filename), "w", encoding="utf-8") as file:
|
||||
file.write(cell["content"])
|
||||
Reference in New Issue
Block a user