Updated
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,158 @@
|
|||||||
|
import math
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pdk_registry import PdkRegistry
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BuildResult:
|
||||||
|
output_path: str
|
||||||
|
engine: str
|
||||||
|
cells_built: List[str] = field(default_factory=list)
|
||||||
|
warnings: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str) -> BuildResult:
|
||||||
|
"""Build a hierarchical project GDS from saved cell YAML files."""
|
||||||
|
cells = _load_project_cells(project_dir)
|
||||||
|
if not cells:
|
||||||
|
raise ValueError("No saved cell YAML files found for this project")
|
||||||
|
|
||||||
|
registry = PdkRegistry(pdk_public_root)
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _build_with_gdstk(cells, output_path, registry)
|
||||||
|
except ImportError as gdstk_error:
|
||||||
|
try:
|
||||||
|
return _build_with_nazca(cells, output_path, registry)
|
||||||
|
except ImportError as nazca_error:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Build GDS requires either gdstk or a working nazca installation. "
|
||||||
|
f"gdstk import failed: {gdstk_error}. nazca import failed: {nazca_error}"
|
||||||
|
) from nazca_error
|
||||||
|
|
||||||
|
|
||||||
|
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
|
||||||
|
cells = {}
|
||||||
|
for filename in sorted(os.listdir(project_dir)):
|
||||||
|
if not filename.lower().endswith((".yml", ".yaml")):
|
||||||
|
continue
|
||||||
|
path = os.path.join(project_dir, filename)
|
||||||
|
with open(path, "r", encoding="utf-8") as file:
|
||||||
|
data = yaml.safe_load(file) or {}
|
||||||
|
cell_name = str(data.get("name") or os.path.splitext(filename)[0])
|
||||||
|
cells[cell_name] = data
|
||||||
|
return cells
|
||||||
|
|
||||||
|
|
||||||
|
def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
|
||||||
|
composites = [name for name, data in cells.items() if data.get("type") != "project"]
|
||||||
|
projects = [name for name, data in cells.items() if data.get("type") == "project"]
|
||||||
|
return composites + projects
|
||||||
|
|
||||||
|
|
||||||
|
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||||
|
import gdstk
|
||||||
|
|
||||||
|
library = gdstk.Library()
|
||||||
|
built_cells = {}
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
for cell_name in _ordered_cell_names(cells):
|
||||||
|
data = cells[cell_name]
|
||||||
|
gds_cell = library.new_cell(_safe_cell_name(cell_name, built_cells))
|
||||||
|
built_cells[cell_name] = gds_cell
|
||||||
|
for instance_name, instance in (data.get("instances") or {}).items():
|
||||||
|
component = str(instance.get("component") or "")
|
||||||
|
x = _number(instance.get("x"))
|
||||||
|
y = _number(instance.get("y"))
|
||||||
|
rotation = math.radians(_number(instance.get("rotation")))
|
||||||
|
child = built_cells.get(component)
|
||||||
|
if child is None:
|
||||||
|
asset = registry.resolve(component)
|
||||||
|
if not asset.gds_path:
|
||||||
|
warnings.append(f"Missing GDS for {instance_name}: {component}")
|
||||||
|
continue
|
||||||
|
child = _import_public_gds(gdstk, library, asset.gds_path)
|
||||||
|
gds_cell.add(gdstk.Reference(child, origin=(x, y), rotation=rotation))
|
||||||
|
|
||||||
|
library.write_gds(output_path)
|
||||||
|
return BuildResult(
|
||||||
|
output_path=output_path,
|
||||||
|
engine="gdstk",
|
||||||
|
cells_built=list(built_cells.keys()),
|
||||||
|
warnings=warnings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_public_gds(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 _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||||
|
import nazca as nd
|
||||||
|
|
||||||
|
warnings = []
|
||||||
|
built_cells = {}
|
||||||
|
ordered_names = _ordered_cell_names(cells)
|
||||||
|
for cell_name in ordered_names:
|
||||||
|
data = cells[cell_name]
|
||||||
|
with nd.Cell(cell_name) as current_cell:
|
||||||
|
for instance_name, instance in (data.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"))
|
||||||
|
if component in built_cells:
|
||||||
|
built_cells[component].put(x, y, rotation)
|
||||||
|
continue
|
||||||
|
asset = registry.resolve(component)
|
||||||
|
if not asset.gds_path:
|
||||||
|
warnings.append(f"Missing GDS for {instance_name}: {component}")
|
||||||
|
continue
|
||||||
|
loaded = nd.load_gds(asset.gds_path)
|
||||||
|
loaded.put(x, y, rotation)
|
||||||
|
built_cells[cell_name] = current_cell
|
||||||
|
|
||||||
|
top_name = ordered_names[-1]
|
||||||
|
nd.export_gds(built_cells[top_name], filename=output_path)
|
||||||
|
return BuildResult(output_path=output_path, engine="nazca", cells_built=ordered_names, warnings=warnings)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_cell_name(name: str, existing: dict) -> str:
|
||||||
|
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell"
|
||||||
|
candidate = base
|
||||||
|
counter = 1
|
||||||
|
used = {cell.name for cell in existing.values()}
|
||||||
|
while candidate in used:
|
||||||
|
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
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PdkAsset:
|
||||||
|
component: str
|
||||||
|
yaml_path: Optional[str] = None
|
||||||
|
gds_path: Optional[str] = None
|
||||||
|
metadata: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PdkRegistry:
|
||||||
|
"""Resolve public PDK component names to metadata and public GDS assets."""
|
||||||
|
|
||||||
|
def __init__(self, public_root: str):
|
||||||
|
self.public_root = os.path.abspath(public_root)
|
||||||
|
self._asset_cache = {}
|
||||||
|
|
||||||
|
def resolve(self, component: str) -> PdkAsset:
|
||||||
|
key = (component or "").strip().replace("\\", "/").strip("/")
|
||||||
|
if not key:
|
||||||
|
return PdkAsset(component=component)
|
||||||
|
if key in self._asset_cache:
|
||||||
|
return self._asset_cache[key]
|
||||||
|
|
||||||
|
yaml_path = self._find_yaml(key)
|
||||||
|
gds_path = self._find_gds(key, yaml_path)
|
||||||
|
metadata = self._load_yaml(yaml_path) if yaml_path else None
|
||||||
|
asset = PdkAsset(component=component, yaml_path=yaml_path, gds_path=gds_path, metadata=metadata)
|
||||||
|
self._asset_cache[key] = asset
|
||||||
|
return asset
|
||||||
|
|
||||||
|
def _find_yaml(self, key: str) -> Optional[str]:
|
||||||
|
direct = os.path.join(self.public_root, *key.split("/"))
|
||||||
|
candidates = []
|
||||||
|
if direct.lower().endswith((".yml", ".yaml")):
|
||||||
|
candidates.append(direct)
|
||||||
|
else:
|
||||||
|
name = key.split("/")[-1]
|
||||||
|
candidates.append(os.path.join(direct, f"{name}.yml"))
|
||||||
|
candidates.append(os.path.join(direct, f"{name}.yaml"))
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
if self._inside_root(candidate) and os.path.exists(candidate):
|
||||||
|
return os.path.abspath(candidate)
|
||||||
|
|
||||||
|
name = key.split("/")[-1]
|
||||||
|
for root, dirs, files in os.walk(self.public_root):
|
||||||
|
if os.path.basename(root) == name:
|
||||||
|
for filename in files:
|
||||||
|
if filename.lower().endswith((".yml", ".yaml")):
|
||||||
|
return os.path.join(root, filename)
|
||||||
|
dirs.clear()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]:
|
||||||
|
search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/"))
|
||||||
|
name = key.split("/")[-1]
|
||||||
|
candidates = [
|
||||||
|
os.path.join(search_dir, f"{name}_BB.gds"),
|
||||||
|
os.path.join(search_dir, f"{name}.gds"),
|
||||||
|
]
|
||||||
|
for candidate in candidates:
|
||||||
|
if self._inside_root(candidate) and os.path.exists(candidate):
|
||||||
|
return os.path.abspath(candidate)
|
||||||
|
if os.path.isdir(search_dir):
|
||||||
|
for filename in sorted(os.listdir(search_dir)):
|
||||||
|
if filename.lower().endswith("_bb.gds"):
|
||||||
|
return os.path.join(search_dir, filename)
|
||||||
|
for filename in sorted(os.listdir(search_dir)):
|
||||||
|
if filename.lower().endswith(".gds"):
|
||||||
|
return os.path.join(search_dir, filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
|
||||||
|
if not yaml_path:
|
||||||
|
return None
|
||||||
|
with open(yaml_path, "r", encoding="utf-8") as file:
|
||||||
|
return yaml.safe_load(file) or {}
|
||||||
|
|
||||||
|
def _inside_root(self, path: str) -> bool:
|
||||||
|
target = os.path.abspath(path)
|
||||||
|
return target == self.public_root or target.startswith(self.public_root + os.sep)
|
||||||
+112
-4
@@ -10,18 +10,29 @@ from flask import Flask, jsonify, send_from_directory, request, redirect, url_fo
|
|||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
import database
|
import database
|
||||||
from flask import Response
|
from flask import Response
|
||||||
|
from gds_builder import build_project_gds
|
||||||
|
from layout_preview import create_layout_svg_from_gds
|
||||||
|
from pdk_registry import PdkRegistry
|
||||||
|
from technology_manifest import TechnologyManifestError, read_technology_manifest
|
||||||
|
|
||||||
# --- Path Configurations ---
|
# --- Path Configurations ---
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend')
|
FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend')
|
||||||
|
|
||||||
|
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', '..'))
|
||||||
|
PDK_PUBLIC_ROOT = os.path.abspath(os.environ.get(
|
||||||
|
'MXPIC_PDK_PUBLIC_ROOT',
|
||||||
|
os.path.join(REPO_ROOT, 'opt_pdk_public', 'foundries')
|
||||||
|
))
|
||||||
|
EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs'))
|
||||||
YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml')
|
YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml')
|
||||||
COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra')
|
COMPS_ROOT = os.environ.get('MXPIC_COMPONENT_ROOT', PDK_PUBLIC_ROOT)
|
||||||
# Define where your new icons folder is located (adjust if it's placed elsewhere)
|
# Define where your new icons folder is located (adjust if it's placed elsewhere)
|
||||||
ICONS_DIR = os.path.join(BASE_DIR, 'icons')
|
ICONS_DIR = os.path.join(BASE_DIR, 'icons')
|
||||||
|
|
||||||
#build layout save path
|
#build layout save path
|
||||||
DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database'))
|
DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database'))
|
||||||
|
PDK_REGISTRY = PdkRegistry(PDK_PUBLIC_ROOT)
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
|
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
|
||||||
@@ -101,6 +112,14 @@ def cell_file_path(project_name, cell_name):
|
|||||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.yml")
|
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.yml")
|
||||||
|
|
||||||
|
|
||||||
|
def cell_svg_path(project_name, cell_name):
|
||||||
|
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg")
|
||||||
|
|
||||||
|
|
||||||
|
def project_gds_path(project_name):
|
||||||
|
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
|
||||||
|
|
||||||
|
|
||||||
def project_meta_path(project_name):
|
def project_meta_path(project_name):
|
||||||
return os.path.join(project_root(project_name), ".project.json")
|
return os.path.join(project_root(project_name), ".project.json")
|
||||||
|
|
||||||
@@ -118,6 +137,14 @@ def write_project_meta(project_name, meta):
|
|||||||
with open(project_meta_path(project_name), 'w', encoding='utf-8') as f:
|
with open(project_meta_path(project_name), 'w', encoding='utf-8') as f:
|
||||||
json.dump(meta, f, indent=2)
|
json.dump(meta, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_project_path(project_name):
|
||||||
|
layout_root = os.path.abspath(user_layout_root())
|
||||||
|
target = os.path.abspath(project_root(project_name))
|
||||||
|
if target != layout_root and not target.startswith(layout_root + os.sep):
|
||||||
|
raise ValueError("Invalid project path")
|
||||||
|
return target
|
||||||
|
|
||||||
# ... [Keep countSpaces and buildTree exactly as they are] ...
|
# ... [Keep countSpaces and buildTree exactly as they are] ...
|
||||||
|
|
||||||
def findComps(baseDir):
|
def findComps(baseDir):
|
||||||
@@ -271,7 +298,7 @@ def health_check():
|
|||||||
def list_technologies():
|
def list_technologies():
|
||||||
"""List technology choices from mxpic/PDKs/<foundry>/<technology>."""
|
"""List technology choices from mxpic/PDKs/<foundry>/<technology>."""
|
||||||
technologies = []
|
technologies = []
|
||||||
pdks_root = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs')
|
pdks_root = EDA_PDK_ROOT
|
||||||
if os.path.isdir(pdks_root):
|
if os.path.isdir(pdks_root):
|
||||||
for foundry in sorted(os.listdir(pdks_root)):
|
for foundry in sorted(os.listdir(pdks_root)):
|
||||||
foundry_path = os.path.join(pdks_root, foundry)
|
foundry_path = os.path.join(pdks_root, foundry)
|
||||||
@@ -290,6 +317,20 @@ def list_technologies():
|
|||||||
return jsonify({"technologies": technologies})
|
return jsonify({"technologies": technologies})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/technologies/<foundry>/<technology>/manifest', methods=['GET'])
|
||||||
|
@login_required_json
|
||||||
|
def get_technology_manifest(foundry, technology):
|
||||||
|
try:
|
||||||
|
manifest = read_technology_manifest(
|
||||||
|
EDA_PDK_ROOT,
|
||||||
|
safe_name(foundry, ''),
|
||||||
|
safe_name(technology, '')
|
||||||
|
)
|
||||||
|
return jsonify({"manifest": manifest})
|
||||||
|
except TechnologyManifestError as e:
|
||||||
|
return jsonify({"error": str(e)}), 404
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/profile', methods=['GET', 'PATCH'])
|
@app.route('/api/profile', methods=['GET', 'PATCH'])
|
||||||
@login_required_json
|
@login_required_json
|
||||||
def account_profile():
|
def account_profile():
|
||||||
@@ -526,18 +567,85 @@ def save_layout():
|
|||||||
with open(save_path, 'w', encoding='utf-8') as f:
|
with open(save_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content)})
|
svg_path = cell_svg_path(project, cell)
|
||||||
|
create_layout_svg_from_gds(content, svg_path, pdk_registry=PDK_REGISTRY, project_dir=project_root(project))
|
||||||
|
|
||||||
|
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "successfully saved",
|
"message": "successfully saved",
|
||||||
"project": project,
|
"project": project,
|
||||||
"cell": cell,
|
"cell": cell,
|
||||||
"path": save_path
|
"path": save_path,
|
||||||
|
"svg_path": svg_path,
|
||||||
|
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell)
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/projects/<project_name>/cells/<cell_name>/layout.svg')
|
||||||
|
@login_required_json
|
||||||
|
def get_layout_svg(project_name, cell_name):
|
||||||
|
try:
|
||||||
|
project_dir = ensure_project_path(project_name)
|
||||||
|
svg_path = os.path.abspath(cell_svg_path(project_name, cell_name))
|
||||||
|
if not svg_path.startswith(project_dir + os.sep):
|
||||||
|
return jsonify({"error": "Invalid SVG path"}), 400
|
||||||
|
if not os.path.exists(svg_path):
|
||||||
|
return jsonify({"error": "Layout SVG not found"}), 404
|
||||||
|
return no_cache_response(send_from_directory(os.path.dirname(svg_path), os.path.basename(svg_path), mimetype='image/svg+xml'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/build-gds', methods=['POST'])
|
||||||
|
@login_required_json
|
||||||
|
def build_gds():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
project = safe_name(data.get('project'), 'project_1')
|
||||||
|
try:
|
||||||
|
project_dir = ensure_project_path(project)
|
||||||
|
if not os.path.isdir(project_dir):
|
||||||
|
return jsonify({"error": "Project not found"}), 404
|
||||||
|
output_path = project_gds_path(project)
|
||||||
|
result = build_project_gds(project_dir, output_path, PDK_PUBLIC_ROOT)
|
||||||
|
record_action('gds.build', project=project, detail={
|
||||||
|
"path": result.output_path,
|
||||||
|
"engine": result.engine,
|
||||||
|
"warnings": result.warnings
|
||||||
|
})
|
||||||
|
return jsonify({
|
||||||
|
"message": "GDS built",
|
||||||
|
"project": project,
|
||||||
|
"path": result.output_path,
|
||||||
|
"gds_url": url_for('get_project_gds', project_name=project, filename=os.path.basename(result.output_path)),
|
||||||
|
"engine": result.engine,
|
||||||
|
"cells_built": result.cells_built,
|
||||||
|
"warnings": result.warnings
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/projects/<project_name>/gds/<filename>')
|
||||||
|
@login_required_json
|
||||||
|
def get_project_gds(project_name, filename):
|
||||||
|
try:
|
||||||
|
project_dir = ensure_project_path(project_name)
|
||||||
|
safe_filename = safe_name(filename, f"{safe_name(project_name, 'project_1')}.gds")
|
||||||
|
if not safe_filename.lower().endswith('.gds'):
|
||||||
|
return jsonify({"error": "Invalid GDS filename"}), 400
|
||||||
|
gds_path = os.path.abspath(os.path.join(project_dir, safe_filename))
|
||||||
|
if not gds_path.startswith(project_dir + os.sep):
|
||||||
|
return jsonify({"error": "Invalid GDS path"}), 400
|
||||||
|
if not os.path.exists(gds_path):
|
||||||
|
return jsonify({"error": "GDS not found"}), 404
|
||||||
|
return send_from_directory(project_dir, safe_filename, as_attachment=True)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- API ROUTES (Library & Components) ---
|
# --- API ROUTES (Library & Components) ---
|
||||||
@app.route('/api/library')
|
@app.route('/api/library')
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class TechnologyManifestError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def technology_manifest_path(pdks_root: str, foundry: str, technology: str) -> str:
|
||||||
|
base = os.path.abspath(pdks_root)
|
||||||
|
path = os.path.abspath(os.path.join(base, foundry, technology, "technology.yml"))
|
||||||
|
if not path.startswith(base + os.sep):
|
||||||
|
raise TechnologyManifestError("Invalid technology path")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def read_technology_manifest(pdks_root: str, foundry: str, technology: str) -> dict:
|
||||||
|
path = technology_manifest_path(pdks_root, foundry, technology)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise TechnologyManifestError("technology manifest not generated; run mxpic_forge technology export workflow")
|
||||||
|
with open(path, "r", encoding="utf-8") as file:
|
||||||
|
manifest = yaml.safe_load(file) or {}
|
||||||
|
if not isinstance(manifest.get("xsections"), dict):
|
||||||
|
raise TechnologyManifestError("technology manifest is missing xsections")
|
||||||
|
if not isinstance(manifest.get("defaults"), dict):
|
||||||
|
raise TechnologyManifestError("technology manifest is missing defaults")
|
||||||
|
return manifest
|
||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="5048.835" height="1611.555" viewBox="1290.0075 -3803.8125 5048.835 1611.555">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
.l1200d0 {stroke: #F38400; fill: #F38400; fill-opacity: 0.5;}
|
||||||
|
.l1205d0 {stroke: #008856; fill: #008856; fill-opacity: 0.5;}
|
||||||
|
.l1001t0 {stroke: none; fill: #A1CAF1;}
|
||||||
|
</style>
|
||||||
|
<g id="Si_EUB_1310_H220_w2000_L50_QY_202604">
|
||||||
|
<polygon id="0000028EA5D10720" class="l1200d0" points="0,-47.54 409.35,-47.54 409.35,361.82 0,361.82"/>
|
||||||
|
<polygon id="0000028EA5D102C0" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
|
||||||
|
<polygon id="0000028EA5D103A0" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
|
||||||
|
<polygon id="0000028EA5D10410" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
|
||||||
|
<polygon id="0000028EA81CD1D0" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
|
||||||
|
<text id="0000028EA5D7B970" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
|
||||||
|
<text id="0000028EA5D7BBB0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b0</text>
|
||||||
|
<text id="0000028EA5D7BE80" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a1</text>
|
||||||
|
<text id="0000028EA5D7D590" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b1</text>
|
||||||
|
</g>
|
||||||
|
<g id="2x2MMI_1310nm_TE_Silterra_202603_ZKY">
|
||||||
|
<polygon id="0000028EA81CD4E0" class="l1200d0" points="-917,-148.25 917,-148.25 917,148.25 -917,148.25"/>
|
||||||
|
<polygon id="0000028EA81CD940" class="l1205d0" points="-913.5,48.25 -920.5,48.25 -920.5,41.25 -913.5,41.25"/>
|
||||||
|
<polygon id="0000028EA81CCDE0" class="l1205d0" points="-913.5,-41.25 -920.5,-41.25 -920.5,-48.25 -913.5,-48.25"/>
|
||||||
|
<polygon id="0000028EA81CD320" class="l1205d0" points="913.5,41.25 920.5,41.25 920.5,48.25 913.5,48.25"/>
|
||||||
|
<polygon id="0000028EA81CCEC0" class="l1205d0" points="913.5,-48.25 920.5,-48.25 920.5,-41.25 913.5,-41.25"/>
|
||||||
|
<polygon id="0000028EA81CDC50" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
|
||||||
|
<polygon id="0000028EA81CD080" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
|
||||||
|
<text id="0000028EA5D7B7C0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 44.75) scale(1 -1)">a1</text>
|
||||||
|
<text id="0000028EA5D7CED0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 -44.75) scale(1 -1)">a2</text>
|
||||||
|
<text id="0000028EA5D7BC40" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 44.75) scale(1 -1)">b1</text>
|
||||||
|
<text id="0000028EA5D7C4B0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 -44.75) scale(1 -1)">b2</text>
|
||||||
|
<text id="0000028EA5D7C420" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
|
||||||
|
<text id="0000028EA5D7C390" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">b0</text>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<rect x="1290.0075" y="-3803.8125" width="5048.835" height="1611.555" fill="#222222" stroke="none"/>
|
||||||
|
<g id="canvas_1" transform="scale(1 -1)">
|
||||||
|
<use transform="translate(5700 3200)" xlink:href="#Si_EUB_1310_H220_w2000_L50_QY_202604"/>
|
||||||
|
<use transform="translate(2440 2570)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,61 @@
|
|||||||
|
# =============================================
|
||||||
|
# mxPIC Cell/Project Definition File
|
||||||
|
# =============================================
|
||||||
|
schema_version: "2.0.0"
|
||||||
|
kind: cell
|
||||||
|
project: mxpic_project_1
|
||||||
|
name: canvas_1
|
||||||
|
type: composite
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
# 1. External Ports (How this cell connects to the outside world)
|
||||||
|
ports:
|
||||||
|
- name: port
|
||||||
|
layer: WG_CORE
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
angle: 0.0
|
||||||
|
width: 0.5
|
||||||
|
|
||||||
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
|
instances:
|
||||||
|
component_5:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/bendings/Si_EUB_1310_H220_w2000_L50_QY_202604
|
||||||
|
x: 570.0
|
||||||
|
y: 320.0
|
||||||
|
rotation: 0.0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
component_6:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
|
||||||
|
x: 244.0
|
||||||
|
y: 257.0
|
||||||
|
rotation: 0.0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
elements:
|
||||||
|
port:
|
||||||
|
type: port
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
angle: 0.0
|
||||||
|
layer: WG_CORE
|
||||||
|
width: 0.5
|
||||||
|
description: ""
|
||||||
|
|
||||||
|
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||||
|
bundles:
|
||||||
|
output_bus:
|
||||||
|
routing_type: euler_bend
|
||||||
|
links:
|
||||||
|
- from: component_5:a1
|
||||||
|
to: component_6:b2
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="4982.835" height="3175.555" viewBox="6753.0075 -5700.8125 4982.835 3175.555">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
.l1200d0 {stroke: #F38400; fill: #F38400; fill-opacity: 0.5;}
|
||||||
|
.l1205d0 {stroke: #008856; fill: #008856; fill-opacity: 0.5;}
|
||||||
|
.l1001t0 {stroke: none; fill: #A1CAF1;}
|
||||||
|
</style>
|
||||||
|
<g id="canvas_1">
|
||||||
|
<use transform="translate(7200 2200)" xlink:href="#Si_EUB_1310_H220_w2000_L50_QY_202604"/>
|
||||||
|
<use transform="translate(4000 1900)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
|
||||||
|
</g>
|
||||||
|
<g id="Si_EUB_1310_H220_w2000_L50_QY_202604">
|
||||||
|
<polygon id="0000019DC7BDA230" class="l1200d0" points="0,-47.54 409.35,-47.54 409.35,361.82 0,361.82"/>
|
||||||
|
<polygon id="0000019DC7BDA310" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
|
||||||
|
<polygon id="0000019DC7BDA460" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
|
||||||
|
<polygon id="0000019DC7BDA4D0" class="l1205d0" points="12.5,12.5 -12.5,12.5 -12.5,-12.5 12.5,-12.5"/>
|
||||||
|
<polygon id="0000019DCA15A120" class="l1205d0" points="374.31,349.32 374.31,374.32 349.31,374.32 349.31,349.32"/>
|
||||||
|
<text id="0000019DC9DB67A0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
|
||||||
|
<text id="0000019DC9DB5990" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b0</text>
|
||||||
|
<text id="0000019DC9DB64D0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a1</text>
|
||||||
|
<text id="0000019DC9DB5A20" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(361.81 361.82) scale(1 -1)">b1</text>
|
||||||
|
</g>
|
||||||
|
<g id="2x2MMI_1310nm_TE_Silterra_202603_ZKY">
|
||||||
|
<polygon id="0000019DCA15A270" class="l1200d0" points="-917,-148.25 917,-148.25 917,148.25 -917,148.25"/>
|
||||||
|
<polygon id="0000019DCA159DA0" class="l1205d0" points="-913.5,48.25 -920.5,48.25 -920.5,41.25 -913.5,41.25"/>
|
||||||
|
<polygon id="0000019DCA159F60" class="l1205d0" points="-913.5,-41.25 -920.5,-41.25 -920.5,-48.25 -913.5,-48.25"/>
|
||||||
|
<polygon id="0000019DCA15A200" class="l1205d0" points="913.5,41.25 920.5,41.25 920.5,48.25 913.5,48.25"/>
|
||||||
|
<polygon id="0000019DCA15A2E0" class="l1205d0" points="913.5,-48.25 920.5,-48.25 920.5,-41.25 913.5,-41.25"/>
|
||||||
|
<polygon id="0000019DCA15A350" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
|
||||||
|
<polygon id="0000019DCA15A4A0" class="l1205d0" points="0,0 0,0 0,0 0,0"/>
|
||||||
|
<text id="0000019DC9DB6050" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 44.75) scale(1 -1)">a1</text>
|
||||||
|
<text id="0000019DC9DB6C20" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(-917 -44.75) scale(1 -1)">a2</text>
|
||||||
|
<text id="0000019DC9DB7130" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 44.75) scale(1 -1)">b1</text>
|
||||||
|
<text id="0000019DC9DB5EA0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(917 -44.75) scale(1 -1)">b2</text>
|
||||||
|
<text id="0000019DC9DB71C0" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">a0</text>
|
||||||
|
<text id="0000019DC9DB7400" class="l1001t0" text-anchor="middle" dominant-baseline="central" transform="translate(0 0) scale(1 -1)">b0</text>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<rect x="6753.0075" y="-5700.8125" width="4982.835" height="3175.555" fill="#222222" stroke="none"/>
|
||||||
|
<g id="mxpic_project_1" transform="scale(1 -1)">
|
||||||
|
<use transform="translate(3900 2900)" xlink:href="#canvas_1"/>
|
||||||
|
<use transform="translate(8400 2900)" xlink:href="#2x2MMI_1310nm_TE_Silterra_202603_ZKY"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -9,67 +9,34 @@ type: project
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
|
||||||
# 1. External Ports (How this cell connects to the outside world)
|
# 1. External Ports (How this cell connects to the outside world)
|
||||||
ports:
|
ports: []
|
||||||
- name: port_3
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 359.0
|
|
||||||
y: 447.0
|
|
||||||
angle: 0.0
|
|
||||||
width: 0.5
|
|
||||||
- name: component_4
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 366.0
|
|
||||||
y: 615.0
|
|
||||||
angle: 0.0
|
|
||||||
width: 0.5
|
|
||||||
|
|
||||||
# 2. Instances (The sub-components dropped onto this canvas)
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
instances:
|
instances:
|
||||||
component_2:
|
canvas_1:
|
||||||
component: EMO1_2ML_CU_Al_RDL/composite/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303
|
component: canvas_1
|
||||||
x: 799.0
|
x: 390.0
|
||||||
y: 420.0
|
y: 290.0
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
mirror: false
|
mirror: false
|
||||||
settings:
|
settings:
|
||||||
length:
|
length:
|
||||||
|
|
||||||
elements:
|
component_7:
|
||||||
anchor_1:
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
|
||||||
type: anchor
|
x: 840.0
|
||||||
x: 479.0
|
y: 290.0
|
||||||
y: 503.0
|
rotation: 0.0
|
||||||
angle: 0.0
|
mirror: false
|
||||||
layer: WG_CORE
|
settings:
|
||||||
width: 0.5
|
length:
|
||||||
description: ""
|
|
||||||
port_3:
|
elements: {}
|
||||||
type: port
|
|
||||||
x: 359.0
|
|
||||||
y: 447.0
|
|
||||||
angle: 0.0
|
|
||||||
layer: WG_CORE
|
|
||||||
width: 0.5
|
|
||||||
description: ""
|
|
||||||
component_4:
|
|
||||||
type: port
|
|
||||||
x: 366.0
|
|
||||||
y: 615.0
|
|
||||||
angle: 0.0
|
|
||||||
layer: WG_CORE
|
|
||||||
width: 0.5
|
|
||||||
description: ""
|
|
||||||
|
|
||||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||||
bundles:
|
bundles:
|
||||||
output_bus:
|
output_bus:
|
||||||
routing_type: euler_bend
|
routing_type: euler_bend
|
||||||
links:
|
links:
|
||||||
- from: anchor_1:right
|
- from: component_7:a1
|
||||||
to: component_2:s1b
|
to: canvas_1:port
|
||||||
- from: anchor_1:left
|
|
||||||
to: port_3:port_3
|
|
||||||
- from: component_2:s1b
|
|
||||||
to: component_2:s1b
|
|
||||||
- from: component_2:g2b
|
|
||||||
to: component_4:component_4
|
|
||||||
Binary file not shown.
@@ -43,11 +43,99 @@
|
|||||||
notes: ''
|
notes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FALLBACK_TECHNOLOGY_MANIFEST = {
|
||||||
|
routing_types: ['euler_bend', 'standard_bend'],
|
||||||
|
defaults: {
|
||||||
|
xsection: 'strip',
|
||||||
|
family: 'optical',
|
||||||
|
width: 0.45,
|
||||||
|
radius: 10,
|
||||||
|
routing_type: 'euler_bend'
|
||||||
|
},
|
||||||
|
xsections: {
|
||||||
|
strip: { family: 'optical', default_width: 0.45, default_radius: 10 },
|
||||||
|
rib_low: { family: 'optical', default_width: 0.45, default_radius: 10 },
|
||||||
|
metal_1: { family: 'electrical', default_width: 5, default_radius: 10 },
|
||||||
|
metal_2: { family: 'electrical', default_width: 5, default_radius: 10 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createForgeArguments = (overrides) => ({
|
const createForgeArguments = (overrides) => ({
|
||||||
...DEFAULT_FORGE_ARGUMENTS,
|
...DEFAULT_FORGE_ARGUMENTS,
|
||||||
...(overrides || {})
|
...(overrides || {})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getTechnologyManifest = (manifest) => manifest || FALLBACK_TECHNOLOGY_MANIFEST;
|
||||||
|
|
||||||
|
const getXsectionInfo = (xsection, manifest) => {
|
||||||
|
const technology = getTechnologyManifest(manifest);
|
||||||
|
return (technology.xsections && technology.xsections[xsection]) || technology.xsections.strip || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRouteSettings = (manifest, overrides) => {
|
||||||
|
const technology = getTechnologyManifest(manifest);
|
||||||
|
const defaults = technology.defaults || FALLBACK_TECHNOLOGY_MANIFEST.defaults;
|
||||||
|
const xsection = (overrides && overrides.xsection) || defaults.xsection || 'strip';
|
||||||
|
const xsectionInfo = getXsectionInfo(xsection, technology);
|
||||||
|
const family = (overrides && overrides.family) || xsectionInfo.family || defaults.family || 'optical';
|
||||||
|
return {
|
||||||
|
xsection,
|
||||||
|
family,
|
||||||
|
width: Number((overrides && overrides.width) ?? xsectionInfo.default_width ?? defaults.width ?? 0.45),
|
||||||
|
radius: Number((overrides && overrides.radius) ?? xsectionInfo.default_radius ?? defaults.radius ?? 10),
|
||||||
|
routing_type: (overrides && overrides.routing_type) || defaults.routing_type || 'euler_bend',
|
||||||
|
widthEdited: Boolean(overrides && overrides.widthEdited)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRouteField = (route, key, value, manifest) => {
|
||||||
|
const current = createRouteSettings(manifest, route);
|
||||||
|
const numericFields = new Set(['width', 'radius']);
|
||||||
|
const nextValue = numericFields.has(key) ? Number(value || 0) : value;
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[key]: nextValue,
|
||||||
|
widthEdited: key === 'width' ? true : current.widthEdited
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRouteXsection = (route, xsection, manifest) => {
|
||||||
|
const technology = getTechnologyManifest(manifest);
|
||||||
|
const current = createRouteSettings(technology, route);
|
||||||
|
const xsectionInfo = getXsectionInfo(xsection, technology);
|
||||||
|
const next = {
|
||||||
|
...current,
|
||||||
|
xsection,
|
||||||
|
family: xsectionInfo.family || current.family
|
||||||
|
};
|
||||||
|
if (!current.widthEdited) {
|
||||||
|
next.width = Number(xsectionInfo.default_width ?? current.width);
|
||||||
|
}
|
||||||
|
next.radius = Number(xsectionInfo.default_radius ?? current.radius);
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeStyleForSettings = (route, selected) => {
|
||||||
|
const settings = createRouteSettings(null, route);
|
||||||
|
const palette = {
|
||||||
|
strip: '#38bdf8',
|
||||||
|
rib_low: '#22c55e',
|
||||||
|
metal_1: '#f59e0b',
|
||||||
|
metal_2: '#f97316'
|
||||||
|
};
|
||||||
|
const electrical = settings.family === 'electrical';
|
||||||
|
const strokeWidth = electrical ? 3.5 : 2.4;
|
||||||
|
return {
|
||||||
|
type: electrical ? 'step' : 'smoothstep',
|
||||||
|
style: {
|
||||||
|
stroke: palette[settings.xsection] || palette.strip,
|
||||||
|
strokeWidth: selected ? strokeWidth + 1.2 : strokeWidth,
|
||||||
|
strokeDasharray: electrical ? '8 5' : undefined,
|
||||||
|
filter: selected ? 'drop-shadow(0 0 5px rgba(255,255,255,0.45))' : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE;
|
const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE;
|
||||||
|
|
||||||
const normalizeAngle = (angle) => {
|
const normalizeAngle = (angle) => {
|
||||||
@@ -272,12 +360,93 @@
|
|||||||
return `elements:\n${lines.join('\n')}`;
|
return `elements:\n${lines.join('\n')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildBundlesYaml = (page, manifest) => {
|
||||||
|
const { nodes = [], edges = [] } = page || {};
|
||||||
|
const nodeMap = {};
|
||||||
|
nodes.forEach(n => { nodeMap[n.id] = n; });
|
||||||
|
|
||||||
|
let linksYaml = '';
|
||||||
|
if (edges.length > 0) {
|
||||||
|
const linkLines = edges.map(edge => {
|
||||||
|
const sourceNode = nodeMap[edge.source];
|
||||||
|
const targetNode = nodeMap[edge.target];
|
||||||
|
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
||||||
|
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
||||||
|
const fromPort = edge.sourceHandle || 'unknown';
|
||||||
|
const toPort = edge.targetHandle || 'unknown';
|
||||||
|
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||||
|
return ` - from: ${sourceName}:${fromPort}
|
||||||
|
to: ${targetName}:${toPort}
|
||||||
|
xsection: ${route.xsection}
|
||||||
|
family: ${route.family}
|
||||||
|
width: ${Number(route.width)}
|
||||||
|
radius: ${Number(route.radius)}
|
||||||
|
routing_type: ${route.routing_type}`;
|
||||||
|
});
|
||||||
|
linksYaml = linkLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||||
|
bundles:
|
||||||
|
output_bus:
|
||||||
|
routing_type: euler_bend
|
||||||
|
links:
|
||||||
|
${linksYaml}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeCenter = (node) => {
|
||||||
|
if (!node) return null;
|
||||||
|
return {
|
||||||
|
x: Number((node.position && node.position.x) || 0),
|
||||||
|
y: Number((node.position && node.position.y) || 0)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const orientation = (a, b, c) => {
|
||||||
|
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
|
||||||
|
if (Math.abs(value) < 1e-9) return 0;
|
||||||
|
return value > 0 ? 1 : 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const segmentsIntersect = (p1, q1, p2, q2) => {
|
||||||
|
if (!p1 || !q1 || !p2 || !q2) return false;
|
||||||
|
const o1 = orientation(p1, q1, p2);
|
||||||
|
const o2 = orientation(p1, q1, q2);
|
||||||
|
const o3 = orientation(p2, q2, p1);
|
||||||
|
const o4 = orientation(p2, q2, q1);
|
||||||
|
return o1 !== o2 && o3 !== o4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSameFamilyRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
|
||||||
|
const candidateRoute = createRouteSettings(manifest, candidateEdge.data && candidateEdge.data.route);
|
||||||
|
const candidateStart = getNodeCenter(nodeMap[candidateEdge.source]);
|
||||||
|
const candidateEnd = getNodeCenter(nodeMap[candidateEdge.target]);
|
||||||
|
for (const edge of existingEdges || []) {
|
||||||
|
if (!edge || edge.id === candidateEdge.id) continue;
|
||||||
|
if (edge.source === candidateEdge.source || edge.source === candidateEdge.target || edge.target === candidateEdge.source || edge.target === candidateEdge.target) continue;
|
||||||
|
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||||
|
if (route.family !== candidateRoute.family) continue;
|
||||||
|
const start = getNodeCenter(nodeMap[edge.source]);
|
||||||
|
const end = getNodeCenter(nodeMap[edge.target]);
|
||||||
|
if (segmentsIntersect(candidateStart, candidateEnd, start, end)) {
|
||||||
|
return { conflictEdge: edge, family: route.family };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
FORGE_COMPONENT_LABEL,
|
FORGE_COMPONENT_LABEL,
|
||||||
FORGE_COMPONENT_TYPE,
|
FORGE_COMPONENT_TYPE,
|
||||||
ELEMENT_COMPONENTS,
|
ELEMENT_COMPONENTS,
|
||||||
DEFAULT_FORGE_ARGUMENTS,
|
DEFAULT_FORGE_ARGUMENTS,
|
||||||
|
FALLBACK_TECHNOLOGY_MANIFEST,
|
||||||
createForgeArguments,
|
createForgeArguments,
|
||||||
|
createRouteSettings,
|
||||||
|
updateRouteField,
|
||||||
|
updateRouteXsection,
|
||||||
|
routeStyleForSettings,
|
||||||
|
findSameFamilyRouteCrossing,
|
||||||
isForgeComponent,
|
isForgeComponent,
|
||||||
normalizeAngle,
|
normalizeAngle,
|
||||||
portSideFromAngle,
|
portSideFromAngle,
|
||||||
@@ -287,6 +456,7 @@
|
|||||||
buildInstancesYaml,
|
buildInstancesYaml,
|
||||||
buildPageComponentPorts,
|
buildPageComponentPorts,
|
||||||
buildCanvasPortsYaml,
|
buildCanvasPortsYaml,
|
||||||
|
buildBundlesYaml,
|
||||||
buildPortsYaml,
|
buildPortsYaml,
|
||||||
buildElementsYaml,
|
buildElementsYaml,
|
||||||
buildSettingsYaml,
|
buildSettingsYaml,
|
||||||
|
|||||||
+572
-109
@@ -18,44 +18,48 @@
|
|||||||
<script src="/canvas-helpers.js"></script>
|
<script src="/canvas-helpers.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-main: #060b16;
|
--bg-main: #05080f;
|
||||||
--bg-card: #0d1626;
|
--bg-card: #0b1320;
|
||||||
--text-main: #f6f8fb;
|
--text-main: #f5f7fa;
|
||||||
--text-muted: #93a3b8;
|
--text-muted: #9aa8ba;
|
||||||
--accent: #6ee7ff;
|
--accent: #45d6c8;
|
||||||
--accent-hover: #7c3aed;
|
--accent-hover: #7fded5;
|
||||||
--accent-green: #34d399;
|
--accent-green: #39d98a;
|
||||||
--accent-warm: #f97316;
|
--accent-warm: #f59e0b;
|
||||||
--border: #28364c;
|
--border: rgba(163, 186, 212, 0.16);
|
||||||
--border-strong: #42516a;
|
--border-strong: rgba(177, 207, 232, 0.32);
|
||||||
--input-bg: #09111f;
|
--input-bg: #070e19;
|
||||||
--panel-rail: #09111f;
|
--panel-rail: #070d16;
|
||||||
--panel-header: #111c2f;
|
--panel-header: #0f1724;
|
||||||
--panel-body: #0d1626;
|
--panel-body: #0a121d;
|
||||||
--canvas-bg: #07101f;
|
--canvas-bg: #060b13;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--folder-icon: #f8c14a;
|
--folder-icon: #f2c14e;
|
||||||
--shadow: rgba(0, 0, 0, 0.34);
|
--shadow: rgba(0, 0, 0, 0.36);
|
||||||
|
--surface-highlight: rgba(255, 255, 255, 0.045);
|
||||||
|
--focus-ring: rgba(69, 214, 200, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode {
|
body.light-mode {
|
||||||
--bg-main: #edf3f8;
|
--bg-main: #eef3f6;
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
--text-main: #132032;
|
--text-main: #122033;
|
||||||
--text-muted: #64758a;
|
--text-muted: #637184;
|
||||||
--accent: #2563eb;
|
--accent: #087f73;
|
||||||
--accent-hover: #0f9f7a;
|
--accent-hover: #0f9d8f;
|
||||||
--accent-green: #16a34a;
|
--accent-green: #14834f;
|
||||||
--accent-warm: #38bdf8;
|
--accent-warm: #c57a00;
|
||||||
--border: #d5e0eb;
|
--border: rgba(30, 48, 69, 0.14);
|
||||||
--border-strong: #b8c7d8;
|
--border-strong: rgba(30, 48, 69, 0.28);
|
||||||
--input-bg: #f4f8fb;
|
--input-bg: #f6f9fb;
|
||||||
--panel-rail: #dde8f2;
|
--panel-rail: #dfe8ee;
|
||||||
--panel-header: #f8fbff;
|
--panel-header: #f8fafc;
|
||||||
--panel-body: #ffffff;
|
--panel-body: #ffffff;
|
||||||
--canvas-bg: #eef5fb;
|
--canvas-bg: #eef4f7;
|
||||||
--folder-icon: #38bdf8;
|
--folder-icon: #0f9d8f;
|
||||||
--shadow: rgba(37, 99, 235, 0.12);
|
--shadow: rgba(18, 32, 51, 0.12);
|
||||||
|
--surface-highlight: rgba(255, 255, 255, 0.72);
|
||||||
|
--focus-ring: rgba(8, 127, 115, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-block {
|
.left-block {
|
||||||
@@ -98,9 +102,9 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
|
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
|
||||||
linear-gradient(0deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px),
|
linear-gradient(0deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
|
||||||
radial-gradient(circle at 18% 8%, rgba(124, 58, 237, 0.14), transparent 26%),
|
linear-gradient(135deg, rgba(69, 214, 200, 0.08), transparent 38%),
|
||||||
var(--canvas-bg);
|
var(--canvas-bg);
|
||||||
background-size: 40px 40px, 40px 40px, auto, auto;
|
background-size: 40px 40px, 40px 40px, auto, auto;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
@@ -110,9 +114,9 @@
|
|||||||
body.light-mode,
|
body.light-mode,
|
||||||
body.light-mode #root {
|
body.light-mode #root {
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(37, 99, 235, 0.04) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(8, 127, 115, 0.045) 1px, transparent 1px),
|
||||||
linear-gradient(0deg, rgba(37, 99, 235, 0.04) 1px, transparent 1px),
|
linear-gradient(0deg, rgba(8, 127, 115, 0.035) 1px, transparent 1px),
|
||||||
radial-gradient(circle at 18% 8%, rgba(56, 189, 248, 0.12), transparent 26%),
|
linear-gradient(135deg, rgba(8, 127, 115, 0.08), transparent 42%),
|
||||||
var(--canvas-bg);
|
var(--canvas-bg);
|
||||||
background-size: 40px 40px, 40px 40px, auto, auto;
|
background-size: 40px 40px, 40px 40px, auto, auto;
|
||||||
}
|
}
|
||||||
@@ -183,13 +187,13 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 8px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent),
|
linear-gradient(180deg, var(--surface-highlight), transparent 74%),
|
||||||
var(--input-bg);
|
var(--input-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.16s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -208,7 +212,8 @@
|
|||||||
.category-card:hover {
|
.category-card:hover {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 12px 22px rgba(37, 99, 235, 0.12);
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-card-icon,
|
.component-card-icon,
|
||||||
@@ -246,13 +251,13 @@
|
|||||||
.left-block,
|
.left-block,
|
||||||
.right-block {
|
.right-block {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.025), transparent),
|
linear-gradient(180deg, var(--surface-highlight), transparent 78%),
|
||||||
var(--panel-body);
|
var(--panel-body);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 14px 30px var(--shadow);
|
box-shadow: 0 18px 38px var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-block-header,
|
.left-block-header,
|
||||||
@@ -268,7 +273,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-block-body,
|
.left-block-body,
|
||||||
@@ -310,6 +315,64 @@
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.build-gds-btn {
|
||||||
|
min-width: 106px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid rgba(69, 214, 200, 0.46);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 11px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #03231f;
|
||||||
|
background: linear-gradient(180deg, #7ee7dc, #35c4b8);
|
||||||
|
box-shadow: 0 10px 24px rgba(24, 166, 153, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-gds-btn::before {
|
||||||
|
content: "";
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 5px 0 0 -2px currentColor, 0 5px 0 -2px currentColor;
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-gds-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.04);
|
||||||
|
box-shadow: 0 14px 30px rgba(24, 166, 153, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-gds-btn:focus-visible,
|
||||||
|
.mini-btn:focus-visible,
|
||||||
|
.toggle-btn:focus-visible {
|
||||||
|
outline: 3px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-gds-btn:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.68;
|
||||||
|
filter: saturate(0.6);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .build-gds-btn {
|
||||||
|
color: #f3fffd;
|
||||||
|
background: linear-gradient(180deg, #0f9d8f, #087f73);
|
||||||
|
box-shadow: 0 10px 22px rgba(8, 127, 115, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
select {
|
select {
|
||||||
@@ -409,6 +472,89 @@
|
|||||||
fill: #0f172a !important;
|
fill: #0f172a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #08111f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
z-index: 12;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(13, 22, 38, 0.92);
|
||||||
|
box-shadow: 0 16px 34px var(--shadow);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-toolbar label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-toolbar input[type="range"] {
|
||||||
|
width: 160px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-toolbar input[type="number"] {
|
||||||
|
width: 74px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 58px 18px 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-scroll-area {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-stage {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .layout-preview {
|
||||||
|
background: #eef4f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .layout-preview-toolbar {
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
.canvas-tab button {
|
.canvas-tab button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -474,21 +620,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mini-btn {
|
.mini-btn {
|
||||||
background: var(--input-bg);
|
background: linear-gradient(180deg, var(--surface-highlight), transparent), var(--input-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 30px;
|
height: 32px;
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-btn:hover {
|
.mini-btn:hover {
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-layout-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid rgba(69, 214, 200, 0.46);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 18px;
|
||||||
|
color: #05231f;
|
||||||
|
background: linear-gradient(180deg, #7ee7dc, #34c5b8);
|
||||||
|
box-shadow: 0 16px 34px rgba(24, 166, 153, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.58);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-layout-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.04);
|
||||||
|
box-shadow: 0 18px 38px rgba(24, 166, 153, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-layout-btn:focus-visible {
|
||||||
|
outline: 3px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .build-layout-btn {
|
||||||
|
color: #f3fffd;
|
||||||
|
background: linear-gradient(180deg, #0f9d8f, #087f73);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-summary-row {
|
.tree-summary-row {
|
||||||
@@ -726,10 +909,10 @@
|
|||||||
height: 112px;
|
height: 112px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #020617;
|
background: linear-gradient(180deg, #040812, #02050b);
|
||||||
color: #c7f9ff;
|
color: #b7efe9;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 8px 12px;
|
padding: 10px 14px;
|
||||||
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
@@ -738,7 +921,29 @@
|
|||||||
|
|
||||||
body.light-mode .app-log-terminal {
|
body.light-mode .app-log-terminal {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #1d4ed8;
|
color: #155e58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chip::before {
|
||||||
|
content: "";
|
||||||
|
width: 20px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -773,7 +978,14 @@
|
|||||||
buildInstancesYaml,
|
buildInstancesYaml,
|
||||||
buildPageComponentPorts,
|
buildPageComponentPorts,
|
||||||
buildCanvasPortsYaml,
|
buildCanvasPortsYaml,
|
||||||
buildElementsYaml
|
buildElementsYaml,
|
||||||
|
buildBundlesYaml: buildRouteBundlesYaml,
|
||||||
|
createRouteSettings,
|
||||||
|
updateRouteField,
|
||||||
|
updateRouteXsection,
|
||||||
|
routeStyleForSettings,
|
||||||
|
findSameFamilyRouteCrossing,
|
||||||
|
FALLBACK_TECHNOLOGY_MANIFEST
|
||||||
} = window.MxpicCanvasHelpers;
|
} = window.MxpicCanvasHelpers;
|
||||||
|
|
||||||
|
|
||||||
@@ -1010,6 +1222,61 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LayoutSvgPreview = ({ page }) => {
|
||||||
|
const [layoutScale, setLayoutScale] = useState(100);
|
||||||
|
const normalizedScale = Math.min(800, Math.max(10, Number(layoutScale) || 100));
|
||||||
|
|
||||||
|
const updateScale = (value) => {
|
||||||
|
setLayoutScale(Math.min(800, Math.max(10, Number(value) || 100)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layout-preview">
|
||||||
|
<div className="layout-preview-toolbar">
|
||||||
|
<label>
|
||||||
|
Scale
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="10"
|
||||||
|
max="800"
|
||||||
|
step="5"
|
||||||
|
value={normalizedScale}
|
||||||
|
onChange={(event) => updateScale(event.target.value)}
|
||||||
|
aria-label="Layout SVG preview scale"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
max="800"
|
||||||
|
step="5"
|
||||||
|
value={normalizedScale}
|
||||||
|
onChange={(event) => updateScale(event.target.value)}
|
||||||
|
aria-label="Layout SVG preview scale percent"
|
||||||
|
/>
|
||||||
|
%
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="layout-preview-canvas">
|
||||||
|
<div className="layout-preview-scroll-area">
|
||||||
|
<div
|
||||||
|
className="layout-preview-stage"
|
||||||
|
style={{ width: `${normalizedScale}%`, height: `${normalizedScale}%` }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="layout-preview-image"
|
||||||
|
src={page.svgUrl}
|
||||||
|
alt={`${page.name} layout preview`}
|
||||||
|
style={{ objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const EditableCanvasTabName = ({ page, active, onRename }) => {
|
const EditableCanvasTabName = ({ page, active, onRename }) => {
|
||||||
const [value, setValue] = useState(page.name);
|
const [value, setValue] = useState(page.name);
|
||||||
|
|
||||||
@@ -1412,7 +1679,7 @@
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => {
|
const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas, onBuildGds, buildGdsBusy, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => {
|
||||||
const [projectPanelHeight, setProjectPanelHeight] = useState(270);
|
const [projectPanelHeight, setProjectPanelHeight] = useState(270);
|
||||||
const [resizingProjectPanel, setResizingProjectPanel] = useState(false);
|
const [resizingProjectPanel, setResizingProjectPanel] = useState(false);
|
||||||
const leftPanelRef = useRef(null);
|
const leftPanelRef = useRef(null);
|
||||||
@@ -1452,10 +1719,15 @@
|
|||||||
<div className="left-block" style={{ display: 'flex', flexDirection: 'column', minHeight: 0, height: projectPanelHeight, flex: '0 0 auto', marginBottom: 0 }}>
|
<div className="left-block" style={{ display: 'flex', flexDirection: 'column', minHeight: 0, height: projectPanelHeight, flex: '0 0 auto', marginBottom: 0 }}>
|
||||||
<div className="left-block-header">
|
<div className="left-block-header">
|
||||||
<span>Project Tree</span>
|
<span>Project Tree</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<button className="build-gds-btn" onClick={onBuildGds} disabled={buildGdsBusy} title="Build project GDS">
|
||||||
|
{buildGdsBusy ? 'Building' : 'Build GDS'}
|
||||||
|
</button>
|
||||||
<button className="toggle-btn" onClick={handleProjectToggle} title={projectExpanded ? 'Collapse all' : 'Expand all'}>
|
<button className="toggle-btn" onClick={handleProjectToggle} title={projectExpanded ? 'Collapse all' : 'Expand all'}>
|
||||||
{projectExpanded ? '-' : '+'}
|
{projectExpanded ? '-' : '+'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="left-block-body project-tree-scroll" style={{ flex: '1 1 0', minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }} key={projectTreeKey} ref={projectTreeRef}>
|
<div className="left-block-body project-tree-scroll" style={{ flex: '1 1 0', minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }} key={projectTreeKey} ref={projectTreeRef}>
|
||||||
{projectTreeItems && projectTreeItems.length > 0 ? (
|
{projectTreeItems && projectTreeItems.length > 0 ? (
|
||||||
projectTreeItems.map(item => {
|
projectTreeItems.map(item => {
|
||||||
@@ -1500,7 +1772,7 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RightPanel = ({ selectedNode, width, onRenameComponent, onUpdateNode }) => {
|
const RightPanel = ({ selectedNode, selectedEdge, technologyManifest, width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
|
||||||
const [componentData, setComponentData] = useState(null);
|
const [componentData, setComponentData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [enlarged, setEnlarged] = useState(null);
|
const [enlarged, setEnlarged] = useState(null);
|
||||||
@@ -1595,6 +1867,63 @@
|
|||||||
const selectedIsPort = selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
|
const selectedIsPort = selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
|
||||||
const selectedIsAnchor = selectedNode?.data?.elementType === 'anchor';
|
const selectedIsAnchor = selectedNode?.data?.elementType === 'anchor';
|
||||||
|
|
||||||
|
if (selectedEdge) {
|
||||||
|
const route = createRouteSettings(technologyManifest, selectedEdge.data?.route);
|
||||||
|
const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
|
||||||
|
const routingTypes = (technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).routing_types || ['euler_bend', 'standard_bend'];
|
||||||
|
return (
|
||||||
|
<aside style={{
|
||||||
|
width: width, background: 'var(--bg-card)', borderLeft: '1px solid var(--border)',
|
||||||
|
padding: 12, display: 'flex', flexDirection: 'column', height: '100%',
|
||||||
|
boxSizing: 'border-box', overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<div className="right-block" style={{ flexShrink: 0 }}>
|
||||||
|
<div className="right-block-header">Route Editor</div>
|
||||||
|
<div className="right-block-body">
|
||||||
|
<div className="route-chip" style={{ color: routeStyleForSettings(route, false).style.stroke }}>
|
||||||
|
{route.family} / {route.xsection}
|
||||||
|
</div>
|
||||||
|
<label>XSection</label>
|
||||||
|
<select
|
||||||
|
value={route.xsection}
|
||||||
|
onChange={(event) => onUpdateEdgeRoute(selectedEdge.id, updateRouteXsection(route, event.target.value, technologyManifest))}
|
||||||
|
>
|
||||||
|
{xsections.map(xsection => (
|
||||||
|
<option key={xsection} value={xsection}>{xsection}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<br /><br />
|
||||||
|
<label>Width</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={route.width}
|
||||||
|
onChange={(event) => onUpdateEdgeRoute(selectedEdge.id, updateRouteField(route, 'width', event.target.value, technologyManifest))}
|
||||||
|
/>
|
||||||
|
<br /><br />
|
||||||
|
<label>Radius</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={route.radius}
|
||||||
|
onChange={(event) => onUpdateEdgeRoute(selectedEdge.id, updateRouteField(route, 'radius', event.target.value, technologyManifest))}
|
||||||
|
/>
|
||||||
|
<br /><br />
|
||||||
|
<label>Routing Type</label>
|
||||||
|
<select
|
||||||
|
value={route.routing_type}
|
||||||
|
onChange={(event) => onUpdateEdgeRoute(selectedEdge.id, updateRouteField(route, 'routing_type', event.target.value, technologyManifest))}
|
||||||
|
>
|
||||||
|
{routingTypes.map(type => (
|
||||||
|
<option key={type} value={type}>{type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updateForgeArgument = (key, value, type) => {
|
const updateForgeArgument = (key, value, type) => {
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
let nextValue = value;
|
let nextValue = value;
|
||||||
@@ -2086,14 +2415,18 @@
|
|||||||
const [gridSnap, setGridSnap] = useState(false);
|
const [gridSnap, setGridSnap] = useState(false);
|
||||||
const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark');
|
const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark');
|
||||||
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]);
|
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]);
|
||||||
|
const [buildGdsBusy, setBuildGdsBusy] = useState(false);
|
||||||
|
const [projectTechnology, setProjectTechnology] = useState('');
|
||||||
|
const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
|
|
||||||
const [clipboard, setClipboard] = useState({ nodes: [] });
|
const [clipboard, setClipboard] = useState({ nodes: [] });
|
||||||
|
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
|
|
||||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||||
const currentNodes = activePage ? activePage.nodes : [];
|
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
|
||||||
const currentEdges = activePage ? activePage.edges : [];
|
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
|
||||||
|
const selectedEdge = useMemo(() => currentEdges.find(edge => edge.selected) || null, [currentEdges]);
|
||||||
|
|
||||||
const [projectCompositeMap, setProjectCompositeMap] = useState({});
|
const [projectCompositeMap, setProjectCompositeMap] = useState({});
|
||||||
const [standaloneComposites, setStandaloneComposites] = useState([]);
|
const [standaloneComposites, setStandaloneComposites] = useState([]);
|
||||||
@@ -2108,6 +2441,30 @@
|
|||||||
setLogs(prev => [...prev.slice(-80), { time: new Date().toLocaleTimeString(), message }]);
|
setLogs(prev => [...prev.slice(-80), { time: new Date().toLocaleTimeString(), message }]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadTechnologyManifest = useCallback(async (technologyId) => {
|
||||||
|
if (!technologyId || !technologyId.includes('/')) {
|
||||||
|
setTechnologyManifest(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
|
return FALLBACK_TECHNOLOGY_MANIFEST;
|
||||||
|
}
|
||||||
|
const [foundry, technology] = technologyId.split('/');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/technologies/${encodeURIComponent(foundry)}/${encodeURIComponent(technology)}/manifest`);
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
addLog(data.error || 'Technology manifest not available; using fallback route defaults.');
|
||||||
|
setTechnologyManifest(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
|
return FALLBACK_TECHNOLOGY_MANIFEST;
|
||||||
|
}
|
||||||
|
const manifest = data.manifest || FALLBACK_TECHNOLOGY_MANIFEST;
|
||||||
|
setTechnologyManifest(manifest);
|
||||||
|
return manifest;
|
||||||
|
} catch (error) {
|
||||||
|
addLog('Technology manifest load failed: ' + error.message);
|
||||||
|
setTechnologyManifest(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
|
return FALLBACK_TECHNOLOGY_MANIFEST;
|
||||||
|
}
|
||||||
|
}, [addLog]);
|
||||||
|
|
||||||
const componentDataCacheRef = useRef(new Map());
|
const componentDataCacheRef = useRef(new Map());
|
||||||
|
|
||||||
const loadComponentMetadata = useCallback(async (componentName) => {
|
const loadComponentMetadata = useCallback(async (componentName) => {
|
||||||
@@ -2195,9 +2552,14 @@
|
|||||||
if (!activePageId) return;
|
if (!activePageId) return;
|
||||||
setPages(prev => prev.map(p => {
|
setPages(prev => prev.map(p => {
|
||||||
if (p.id !== activePageId) return p;
|
if (p.id !== activePageId) return p;
|
||||||
return { ...p, edges: applyEdgeChanges(changes, p.edges) };
|
const styledEdges = applyEdgeChanges(changes, p.edges).map(edge => {
|
||||||
|
const route = createRouteSettings(technologyManifest, edge.data?.route);
|
||||||
|
const view = routeStyleForSettings(route, edge.selected);
|
||||||
|
return { ...edge, type: view.type, style: view.style, data: { ...edge.data, route } };
|
||||||
|
});
|
||||||
|
return { ...p, edges: styledEdges };
|
||||||
}));
|
}));
|
||||||
}, [activePageId]);
|
}, [activePageId, technologyManifest]);
|
||||||
|
|
||||||
const handleUpdateNode = useCallback((nodeId, update) => {
|
const handleUpdateNode = useCallback((nodeId, update) => {
|
||||||
if (!activePageId) return;
|
if (!activePageId) return;
|
||||||
@@ -2224,6 +2586,31 @@
|
|||||||
}));
|
}));
|
||||||
}, [activePageId]);
|
}, [activePageId]);
|
||||||
|
|
||||||
|
const handleUpdateEdgeRoute = useCallback((edgeId, nextRoute) => {
|
||||||
|
if (!activePageId) return;
|
||||||
|
setPages(prev => prev.map(p => {
|
||||||
|
if (p.id !== activePageId) return p;
|
||||||
|
const nodeMap = Object.fromEntries(p.nodes.map(node => [node.id, node]));
|
||||||
|
let rejected = false;
|
||||||
|
const nextEdges = p.edges.map(edge => {
|
||||||
|
if (edge.id !== edgeId) return edge;
|
||||||
|
const route = createRouteSettings(technologyManifest, nextRoute);
|
||||||
|
const view = routeStyleForSettings(route, edge.selected);
|
||||||
|
const candidate = { ...edge, type: view.type, style: view.style, data: { ...edge.data, route } };
|
||||||
|
const conflict = findSameFamilyRouteCrossing(candidate, p.edges, nodeMap, technologyManifest);
|
||||||
|
if (conflict) {
|
||||||
|
const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source;
|
||||||
|
const target = nodeMap[conflict.conflictEdge.target]?.data?.componentDisplayName || conflict.conflictEdge.target;
|
||||||
|
addLog(`Route update rejected: ${route.family} route crosses ${source} to ${target}.`);
|
||||||
|
rejected = true;
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
});
|
||||||
|
return rejected ? p : { ...p, edges: nextEdges };
|
||||||
|
}));
|
||||||
|
}, [activePageId, technologyManifest, addLog]);
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
if (!activePage) return;
|
if (!activePage) return;
|
||||||
const selectedNodes = activePage.nodes.filter(n => n.selected);
|
const selectedNodes = activePage.nodes.filter(n => n.selected);
|
||||||
@@ -2436,14 +2823,17 @@
|
|||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetId = nodeNameMap[toInst];
|
||||||
if (sourceId && targetId) {
|
if (sourceId && targetId) {
|
||||||
|
const route = createRouteSettings(technologyManifest, link);
|
||||||
|
const view = routeStyleForSettings(route, false);
|
||||||
newEdges.push({
|
newEdges.push({
|
||||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
target: targetId,
|
target: targetId,
|
||||||
sourceHandle: fromPort,
|
sourceHandle: fromPort,
|
||||||
targetHandle: toPort,
|
targetHandle: toPort,
|
||||||
type: 'smoothstep',
|
type: view.type,
|
||||||
style: { stroke: 'var(--accent)', strokeWidth: 2 },
|
style: view.style,
|
||||||
|
data: { route },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2524,7 +2914,7 @@
|
|||||||
|
|
||||||
input.addEventListener('change', handleFile);
|
input.addEventListener('change', handleFile);
|
||||||
return () => input.removeEventListener('change', handleFile);
|
return () => input.removeEventListener('change', handleFile);
|
||||||
}, [library]);
|
}, [library, technologyManifest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectCompositeMap(prev => {
|
setProjectCompositeMap(prev => {
|
||||||
@@ -2570,7 +2960,7 @@
|
|||||||
return category;
|
return category;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageFromYaml = (cellName, content) => {
|
const pageFromYaml = (cellName, content, manifest) => {
|
||||||
const doc = jsyaml.load(content) || {};
|
const doc = jsyaml.load(content) || {};
|
||||||
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
||||||
const pagePort = firstPort
|
const pagePort = firstPort
|
||||||
@@ -2627,14 +3017,17 @@
|
|||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetId = nodeNameMap[toInst];
|
||||||
if (!sourceId || !targetId) return;
|
if (!sourceId || !targetId) return;
|
||||||
|
const route = createRouteSettings(manifest, link);
|
||||||
|
const view = routeStyleForSettings(route, false);
|
||||||
edges.push({
|
edges.push({
|
||||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
target: targetId,
|
target: targetId,
|
||||||
sourceHandle: fromPort,
|
sourceHandle: fromPort,
|
||||||
targetHandle: toPort,
|
targetHandle: toPort,
|
||||||
type: 'smoothstep',
|
type: view.type,
|
||||||
style: { stroke: 'var(--accent)', strokeWidth: 2 },
|
style: view.style,
|
||||||
|
data: { route },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2654,6 +3047,8 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/projects/${encodeURIComponent(currentProjectName)}`);
|
const response = await fetch(`/api/projects/${encodeURIComponent(currentProjectName)}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
setProjectTechnology('');
|
||||||
|
setTechnologyManifest(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
setPages([projectPage]);
|
setPages([projectPage]);
|
||||||
setActivePageId(projectPage.id);
|
setActivePageId(projectPage.id);
|
||||||
setProjectCompositeMap({ [currentProjectName]: [] });
|
setProjectCompositeMap({ [currentProjectName]: [] });
|
||||||
@@ -2661,7 +3056,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const cellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content));
|
const technology = data.technology || '';
|
||||||
|
setProjectTechnology(technology);
|
||||||
|
const manifest = await loadTechnologyManifest(technology);
|
||||||
|
const cellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content, manifest));
|
||||||
setPages([projectPage, ...cellPages]);
|
setPages([projectPage, ...cellPages]);
|
||||||
setActivePageId(projectPage.id);
|
setActivePageId(projectPage.id);
|
||||||
setProjectCompositeMap({ [currentProjectName]: cellPages.map(page => page.name) });
|
setProjectCompositeMap({ [currentProjectName]: cellPages.map(page => page.name) });
|
||||||
@@ -2672,6 +3070,8 @@
|
|||||||
});
|
});
|
||||||
setCompositeTrees(nextTrees);
|
setCompositeTrees(nextTrees);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setProjectTechnology('');
|
||||||
|
setTechnologyManifest(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
setPages([projectPage]);
|
setPages([projectPage]);
|
||||||
setActivePageId(projectPage.id);
|
setActivePageId(projectPage.id);
|
||||||
setProjectCompositeMap({ [currentProjectName]: [] });
|
setProjectCompositeMap({ [currentProjectName]: [] });
|
||||||
@@ -2679,7 +3079,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadProject();
|
loadProject();
|
||||||
}, [library, currentProjectName]);
|
}, [library, currentProjectName, loadTechnologyManifest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activePage && reactFlowInstance) {
|
if (activePage && reactFlowInstance) {
|
||||||
@@ -3241,9 +3641,26 @@
|
|||||||
if (!activePageId) return;
|
if (!activePageId) return;
|
||||||
setPages(prev => prev.map(p => {
|
setPages(prev => prev.map(p => {
|
||||||
if (p.id !== activePageId) return p;
|
if (p.id !== activePageId) return p;
|
||||||
return { ...p, edges: addEdge({ ...connection, type: 'smoothstep', style: { stroke: 'var(--accent)', strokeWidth: 2 } }, p.edges) };
|
const route = createRouteSettings(technologyManifest);
|
||||||
|
const view = routeStyleForSettings(route, false);
|
||||||
|
const candidate = {
|
||||||
|
...connection,
|
||||||
|
id: `edge-${connection.source}-${connection.sourceHandle || 'port'}-${connection.target}-${connection.targetHandle || 'port'}-${Date.now()}`,
|
||||||
|
type: view.type,
|
||||||
|
style: view.style,
|
||||||
|
data: { route },
|
||||||
|
};
|
||||||
|
const nodeMap = Object.fromEntries(p.nodes.map(node => [node.id, node]));
|
||||||
|
const conflict = findSameFamilyRouteCrossing(candidate, p.edges, nodeMap, technologyManifest);
|
||||||
|
if (conflict) {
|
||||||
|
const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source;
|
||||||
|
const target = nodeMap[conflict.conflictEdge.target]?.data?.componentDisplayName || conflict.conflictEdge.target;
|
||||||
|
addLog(`Connection rejected: ${route.family} route crosses ${source} to ${target}.`);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return { ...p, edges: addEdge(candidate, p.edges) };
|
||||||
}));
|
}));
|
||||||
}, [activePageId]);
|
}, [activePageId, technologyManifest, addLog]);
|
||||||
|
|
||||||
const expandAll = useCallback(() => {
|
const expandAll = useCallback(() => {
|
||||||
if (treeContainerRef.current) {
|
if (treeContainerRef.current) {
|
||||||
@@ -3399,35 +3816,55 @@
|
|||||||
};
|
};
|
||||||
}, [pages, library]);
|
}, [pages, library]);
|
||||||
|
|
||||||
const buildBundlesYaml = (page) => {
|
const buildBundlesYaml = useCallback((page) => {
|
||||||
const { nodes, edges } = page;
|
return buildRouteBundlesYaml(page, technologyManifest);
|
||||||
const nodeMap = {};
|
}, [technologyManifest]);
|
||||||
nodes.forEach(n => { nodeMap[n.id] = n; });
|
|
||||||
|
|
||||||
let linksYaml = '';
|
const validateRouteCrossings = useCallback((page) => {
|
||||||
if (edges.length > 0) {
|
if (!page || !Array.isArray(page.edges)) return true;
|
||||||
const linkLines = edges.map(edge => {
|
const nodeMap = Object.fromEntries((page.nodes || []).map(node => [node.id, node]));
|
||||||
const sourceNode = nodeMap[edge.source];
|
for (const edge of page.edges) {
|
||||||
const targetNode = nodeMap[edge.target];
|
const conflict = findSameFamilyRouteCrossing(edge, page.edges, nodeMap, technologyManifest);
|
||||||
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
if (conflict) {
|
||||||
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
const route = createRouteSettings(technologyManifest, edge.data?.route);
|
||||||
const fromPort = edge.sourceHandle || 'unknown';
|
const source = nodeMap[edge.source]?.data?.componentDisplayName || edge.source;
|
||||||
const toPort = edge.targetHandle || 'unknown';
|
const target = nodeMap[edge.target]?.data?.componentDisplayName || edge.target;
|
||||||
return ` - from: ${sourceName}:${fromPort}\n to: ${targetName}:${toPort}`;
|
const conflictSource = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source;
|
||||||
});
|
const conflictTarget = nodeMap[conflict.conflictEdge.target]?.data?.componentDisplayName || conflict.conflictEdge.target;
|
||||||
linksYaml = linkLines.join('\n');
|
addLog(`Build blocked: ${route.family} route ${source} to ${target} crosses ${conflictSource} to ${conflictTarget}.`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [technologyManifest, addLog]);
|
||||||
|
|
||||||
return `# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
const openLayoutPreview = useCallback((cellName, svgUrl) => {
|
||||||
bundles:
|
if (!cellName || !svgUrl) return;
|
||||||
output_bus:
|
const layoutTabId = `layout-${currentProjectName}-${cellName}`;
|
||||||
routing_type: euler_bend
|
setPages(prev => {
|
||||||
links:
|
const existing = prev.find(page => page.id === layoutTabId);
|
||||||
${linksYaml}`;
|
if (existing) {
|
||||||
};
|
return prev.map(page => page.id === layoutTabId
|
||||||
|
? { ...page, name: `${cellName}:layout`, type: 'layoutPreview', svgUrl, nodes: [], edges: [], isClosed: false }
|
||||||
|
: page
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return prev.concat({
|
||||||
|
id: layoutTabId,
|
||||||
|
name: `${cellName}:layout`,
|
||||||
|
type: 'layoutPreview',
|
||||||
|
svgUrl,
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
isClosed: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setActivePageId(layoutTabId);
|
||||||
|
}, [currentProjectName]);
|
||||||
|
|
||||||
const handleBuildLayout = useCallback(async () => {
|
const handleBuildLayout = useCallback(async () => {
|
||||||
if (!activePage) return;
|
if (!activePage) return;
|
||||||
|
if (!validateRouteCrossings(activePage)) return;
|
||||||
const header = `# =============================================
|
const header = `# =============================================
|
||||||
# mxPIC Cell/Project Definition File
|
# mxPIC Cell/Project Definition File
|
||||||
# =============================================
|
# =============================================
|
||||||
@@ -3483,10 +3920,41 @@ ${bundlesBlock}`;
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
addLog('Successfully saved: ' + result.path);
|
addLog('Successfully saved: ' + result.path);
|
||||||
|
if (result.svg_url) {
|
||||||
|
openLayoutPreview(activePage.name, result.svg_url);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addLog('Save error: ' + err.message);
|
addLog('Save error: ' + err.message);
|
||||||
}
|
}
|
||||||
}, [activePage, library, buildBundlesYaml, findComponentPath, currentProjectName, addLog]);
|
}, [activePage, library, buildBundlesYaml, findComponentPath, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings]);
|
||||||
|
|
||||||
|
const handleBuildGds = useCallback(async () => {
|
||||||
|
if (buildGdsBusy) return;
|
||||||
|
const invalidPage = pages.find(page => page.type !== 'layoutPreview' && !validateRouteCrossings(page));
|
||||||
|
if (invalidPage) return;
|
||||||
|
setBuildGdsBusy(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/build-gds', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ project: currentProjectName }),
|
||||||
|
});
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
const detail = result.error ? `: ${result.error}` : ` (HTTP ${response.status})`;
|
||||||
|
addLog(`Build GDS failed${detail}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const warningText = result.warnings && result.warnings.length > 0
|
||||||
|
? ` (${result.warnings.length} warnings)`
|
||||||
|
: '';
|
||||||
|
addLog(`GDS built with ${result.engine}: ${result.path}${warningText}`);
|
||||||
|
} catch (err) {
|
||||||
|
addLog(`Build GDS network error: ${err.message}. Check that the Flask server is running from the same host and Python environment.`);
|
||||||
|
} finally {
|
||||||
|
setBuildGdsBusy(false);
|
||||||
|
}
|
||||||
|
}, [buildGdsBusy, currentProjectName, addLog, pages, validateRouteCrossings]);
|
||||||
|
|
||||||
const onNodeDoubleClick = useCallback((event, node) => {
|
const onNodeDoubleClick = useCallback((event, node) => {
|
||||||
if (node.data?.type === 'composite') {
|
if (node.data?.type === 'composite') {
|
||||||
@@ -3505,6 +3973,8 @@ ${bundlesBlock}`;
|
|||||||
onSelectInstance={selectInstanceInPage}
|
onSelectInstance={selectInstanceInPage}
|
||||||
onRenameCanvas={renameCanvas}
|
onRenameCanvas={renameCanvas}
|
||||||
onDeleteCanvas={deleteCanvas}
|
onDeleteCanvas={deleteCanvas}
|
||||||
|
onBuildGds={handleBuildGds}
|
||||||
|
buildGdsBusy={buildGdsBusy}
|
||||||
projectExpanded={projectExpanded}
|
projectExpanded={projectExpanded}
|
||||||
onProjectToggle={handleProjectToggle}
|
onProjectToggle={handleProjectToggle}
|
||||||
projectTreeRef={projectTreeContainerRef}
|
projectTreeRef={projectTreeContainerRef}
|
||||||
@@ -3560,29 +4030,18 @@ ${bundlesBlock}`;
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activePage && (
|
{activePage && activePage.type !== 'layoutPreview' && (
|
||||||
<button
|
<button
|
||||||
onClick={handleBuildLayout}
|
onClick={handleBuildLayout}
|
||||||
style={{
|
className="build-layout-btn"
|
||||||
position: 'absolute',
|
|
||||||
bottom: 20,
|
|
||||||
right: 20,
|
|
||||||
zIndex: 10,
|
|
||||||
background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
|
|
||||||
color: '#04101f',
|
|
||||||
border: 'none',
|
|
||||||
padding: '12px 20px',
|
|
||||||
borderRadius: 8,
|
|
||||||
fontWeight: '700',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '0.85em',
|
|
||||||
boxShadow: '0 16px 34px rgba(37,99,235,0.24)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Build Layout
|
Build Layout
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activePage && activePage.type === 'layoutPreview' ? (
|
||||||
|
<LayoutSvgPreview page={activePage} />
|
||||||
|
) : (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={currentNodes}
|
nodes={currentNodes}
|
||||||
edges={currentEdges}
|
edges={currentEdges}
|
||||||
@@ -3603,6 +4062,7 @@ ${bundlesBlock}`;
|
|||||||
<Controls style={{ bottom: 15, left: 15 }} />
|
<Controls style={{ bottom: 15, left: 15 }} />
|
||||||
<Background color="#334155" gap={20} size={1} />
|
<Background color="#334155" gap={20} size={1} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="app-log-terminal">
|
<div className="app-log-terminal">
|
||||||
{logs.map((entry, index) => (
|
{logs.map((entry, index) => (
|
||||||
@@ -3614,9 +4074,12 @@ ${bundlesBlock}`;
|
|||||||
<ResizeHandle onMouseDown={handleResizeStart('right')} />
|
<ResizeHandle onMouseDown={handleResizeStart('right')} />
|
||||||
<RightPanel
|
<RightPanel
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
|
selectedEdge={selectedEdge}
|
||||||
|
technologyManifest={technologyManifest}
|
||||||
width={rightWidth}
|
width={rightWidth}
|
||||||
onRenameComponent={renameComponent}
|
onRenameComponent={renameComponent}
|
||||||
onUpdateNode={handleUpdateNode}
|
onUpdateNode={handleUpdateNode}
|
||||||
|
onUpdateEdgeRoute={handleUpdateEdgeRoute}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
schema_version: 1.0.0
|
||||||
|
foundry: Silterra
|
||||||
|
technology: EMO1_2ML_CU_Al_RDL
|
||||||
|
source_class: mxpic.foundries.Silterra.EOM1_2ML_CU_RDL
|
||||||
|
constants:
|
||||||
|
STD_SMWG_WIDTH: 0.45
|
||||||
|
SLAB_GROWTH: 2
|
||||||
|
W_METAL_MIN: 5
|
||||||
|
SPACING_HEATER_MIN: 2
|
||||||
|
SPACING_METAL_MIN: 4
|
||||||
|
W_HEATER_MIN: 3
|
||||||
|
layers:
|
||||||
|
WG_HM: {layer: 275, datatype: 0}
|
||||||
|
WG_STRIP: {layer: 101, datatype: 251}
|
||||||
|
WG_LOWRIB: {layer: 100, datatype: 90}
|
||||||
|
WG_HIGHRIB: {layer: 232, datatype: 0}
|
||||||
|
HEATER: {layer: 29, datatype: 30}
|
||||||
|
CT_SI: {layer: 268, datatype: 0}
|
||||||
|
CT_GE: {layer: 35, datatype: 0}
|
||||||
|
UTV: {layer: 172, datatype: 0}
|
||||||
|
RDL_VIA: {layer: 194, datatype: 0}
|
||||||
|
UTM: {layer: 173, datatype: 0}
|
||||||
|
UTM2: {layer: 197, datatype: 0}
|
||||||
|
RDL_MET: {layer: 195, datatype: 0}
|
||||||
|
PAD_ELE: {layer: 100, datatype: 170}
|
||||||
|
PAD_OPTICAL: {layer: 100, datatype: 160}
|
||||||
|
PAD_AL: {layer: 145, datatype: 0}
|
||||||
|
WG_N: {layer: 263, datatype: 0}
|
||||||
|
SiN_Rib_WG: {layer: 63, datatype: 30}
|
||||||
|
SSIN0: {layer: 283, datatype: 0}
|
||||||
|
SSIN1: {layer: 289, datatype: 0}
|
||||||
|
SSIN2: {layer: 290, datatype: 0}
|
||||||
|
SSIN3: {layer: 291, datatype: 0}
|
||||||
|
routing_types:
|
||||||
|
- euler_bend
|
||||||
|
- standard_bend
|
||||||
|
defaults:
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
xsections:
|
||||||
|
strip:
|
||||||
|
family: optical
|
||||||
|
default_width: 0.45
|
||||||
|
default_radius: 10
|
||||||
|
layers:
|
||||||
|
- {layer: WG_HM, growx: 0, growy: 0}
|
||||||
|
- {layer: WG_STRIP, growx: 4, growy: 4}
|
||||||
|
rib_low:
|
||||||
|
family: optical
|
||||||
|
default_width: 0.45
|
||||||
|
default_radius: 10
|
||||||
|
layers:
|
||||||
|
- {layer: WG_HM, growx: 0, growy: 0}
|
||||||
|
- {layer: WG_SRIB, growx: 3, growy: 3}
|
||||||
|
- {layer: WG_STRIP, leftedge: [-0.5, -3], rightedge: [-0.5, -3.5]}
|
||||||
|
- {layer: WG_STRIP, leftedge: [0.5, 3.5], rightedge: [0.5, 3]}
|
||||||
|
metal_1:
|
||||||
|
family: electrical
|
||||||
|
default_width: 5
|
||||||
|
default_radius: 10
|
||||||
|
layers:
|
||||||
|
- {layer: UTM, growx: 0, growy: 0}
|
||||||
|
- {layer: SSIN0, growx: 2.5, growy: 2.5}
|
||||||
|
metal_2:
|
||||||
|
family: electrical
|
||||||
|
default_width: 5
|
||||||
|
default_radius: 10
|
||||||
|
layers:
|
||||||
|
- {layer: UTM2, growx: 0, growy: 0}
|
||||||
|
- {layer: SSIN1, growx: 2.5, growy: 2.5}
|
||||||
@@ -145,3 +145,87 @@ assert(instancesWithoutElements.includes('component_1:'));
|
|||||||
|
|
||||||
const multiPortComponentPorts = helpers.buildPageComponentPorts(null, elementNodes);
|
const multiPortComponentPorts = helpers.buildPageComponentPorts(null, elementNodes);
|
||||||
assert.deepStrictEqual(multiPortComponentPorts.in0, { x: 10, y: 20, a: 180, width: 0.7 });
|
assert.deepStrictEqual(multiPortComponentPorts.in0, { x: 10, y: 20, a: 180, width: 0.7 });
|
||||||
|
|
||||||
|
const technologyManifest = {
|
||||||
|
defaults: { xsection: 'strip', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
||||||
|
xsections: {
|
||||||
|
strip: { family: 'optical', default_width: 0.45 },
|
||||||
|
rib_low: { family: 'optical', default_width: 0.5 },
|
||||||
|
metal_1: { family: 'electrical', default_width: 5 },
|
||||||
|
metal_2: { family: 'electrical', default_width: 6 }
|
||||||
|
},
|
||||||
|
routing_types: ['euler_bend', 'standard_bend']
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeDefaults = helpers.createRouteSettings(technologyManifest);
|
||||||
|
assert.deepStrictEqual(routeDefaults, {
|
||||||
|
xsection: 'strip',
|
||||||
|
family: 'optical',
|
||||||
|
width: 0.45,
|
||||||
|
radius: 10,
|
||||||
|
routing_type: 'euler_bend',
|
||||||
|
widthEdited: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const metalRoute = helpers.updateRouteXsection(routeDefaults, 'metal_1', technologyManifest);
|
||||||
|
assert.strictEqual(metalRoute.family, 'electrical');
|
||||||
|
assert.strictEqual(metalRoute.width, 5);
|
||||||
|
|
||||||
|
const manuallyEditedWidth = helpers.updateRouteField(routeDefaults, 'width', 0.62, technologyManifest);
|
||||||
|
const changedXsection = helpers.updateRouteXsection(manuallyEditedWidth, 'rib_low', technologyManifest);
|
||||||
|
assert.strictEqual(changedXsection.width, 0.62);
|
||||||
|
assert.strictEqual(changedXsection.family, 'optical');
|
||||||
|
|
||||||
|
const styledStrip = helpers.routeStyleForSettings({ xsection: 'strip', family: 'optical' }, false);
|
||||||
|
const styledMetal = helpers.routeStyleForSettings({ xsection: 'metal_1', family: 'electrical' }, true);
|
||||||
|
assert.notStrictEqual(styledStrip.style.stroke, styledMetal.style.stroke);
|
||||||
|
assert(styledMetal.style.strokeDasharray, 'electrical routes should use a visibly different line treatment');
|
||||||
|
assert(styledMetal.style.strokeWidth > styledStrip.style.strokeWidth);
|
||||||
|
|
||||||
|
const routeYaml = helpers.buildBundlesYaml({
|
||||||
|
nodes: [
|
||||||
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
||||||
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } }
|
||||||
|
],
|
||||||
|
edges: [{
|
||||||
|
id: 'edge-a-b',
|
||||||
|
source: 'a',
|
||||||
|
target: 'b',
|
||||||
|
sourceHandle: 'out',
|
||||||
|
targetHandle: 'in',
|
||||||
|
data: { route: { xsection: 'metal_1', family: 'electrical', width: 5, radius: 20, routing_type: 'standard_bend' } }
|
||||||
|
}]
|
||||||
|
}, technologyManifest);
|
||||||
|
assert(routeYaml.includes('xsection: metal_1'));
|
||||||
|
assert(routeYaml.includes('family: electrical'));
|
||||||
|
assert(routeYaml.includes('radius: 20'));
|
||||||
|
assert(routeYaml.includes('routing_type: standard_bend'));
|
||||||
|
|
||||||
|
const edgeA = {
|
||||||
|
id: 'edge-a-b',
|
||||||
|
source: 'a',
|
||||||
|
target: 'b',
|
||||||
|
data: { route: { family: 'optical' } }
|
||||||
|
};
|
||||||
|
const edgeB = {
|
||||||
|
id: 'edge-c-d',
|
||||||
|
source: 'c',
|
||||||
|
target: 'd',
|
||||||
|
data: { route: { family: 'optical' } }
|
||||||
|
};
|
||||||
|
const edgeC = {
|
||||||
|
id: 'edge-e-f',
|
||||||
|
source: 'e',
|
||||||
|
target: 'f',
|
||||||
|
data: { route: { family: 'electrical' } }
|
||||||
|
};
|
||||||
|
const crossingNodes = {
|
||||||
|
a: { position: { x: 0, y: 0 } },
|
||||||
|
b: { position: { x: 100, y: 100 } },
|
||||||
|
c: { position: { x: 0, y: 100 } },
|
||||||
|
d: { position: { x: 100, y: 0 } },
|
||||||
|
e: { position: { x: 0, y: 100 } },
|
||||||
|
f: { position: { x: 100, y: 0 } }
|
||||||
|
};
|
||||||
|
assert.strictEqual(helpers.findSameFamilyRouteCrossing(edgeB, [edgeA], crossingNodes).conflictEdge.id, 'edge-a-b');
|
||||||
|
assert.strictEqual(helpers.findSameFamilyRouteCrossing(edgeC, [edgeA], crossingNodes), null);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const backendDir = path.join(root, 'backend');
|
||||||
|
const serverPy = fs.readFileSync(path.join(backendDir, 'server.py'), 'utf8');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backendDir, 'layout_preview.py')),
|
||||||
|
'backend/layout_preview.py should generate SVG previews from saved layout YAML'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backendDir, 'pdk_registry.py')),
|
||||||
|
'backend/pdk_registry.py should resolve public PDK YAML/GDS assets'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backendDir, 'gds_builder.py')),
|
||||||
|
'backend/gds_builder.py should build hierarchical GDS from saved project YAML'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('create_layout_svg_from_gds'),
|
||||||
|
'save-layout route should create a GDS-derived layout SVG preview'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('svg_url'),
|
||||||
|
'save-layout response should include an svg_url for the new layout tab'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes("@app.route('/api/projects/<project_name>/cells/<cell_name>/layout.svg')"),
|
||||||
|
'server should expose a route for saved cell SVG previews'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes("@app.route('/api/build-gds'"),
|
||||||
|
'server should expose a Build GDS API route'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes("@app.route('/api/technologies/<foundry>/<technology>/manifest'"),
|
||||||
|
'server should expose a technology manifest API route'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backendDir, 'technology_manifest.py')),
|
||||||
|
'backend/technology_manifest.py should read generated technology manifests'
|
||||||
|
);
|
||||||
|
|
||||||
|
const techManifestPath = path.join(root, 'mxpic', 'PDKs', 'Silterra', 'EMO1_2ML_CU_Al_RDL', 'technology.yml');
|
||||||
|
assert(
|
||||||
|
fs.existsSync(techManifestPath),
|
||||||
|
'Silterra technology.yml should be generated into the EDA PDK folder'
|
||||||
|
);
|
||||||
|
const techManifest = fs.readFileSync(techManifestPath, 'utf8');
|
||||||
|
for (const xsection of ['strip', 'rib_low', 'metal_1', 'metal_2']) {
|
||||||
|
assert(techManifest.includes(`${xsection}:`), `technology.yml should include ${xsection}`);
|
||||||
|
}
|
||||||
|
assert(techManifest.includes('family: optical'), 'technology.yml should classify optical xsections');
|
||||||
|
assert(techManifest.includes('family: electrical'), 'technology.yml should classify electrical xsections');
|
||||||
|
|
||||||
|
const layoutPreviewPy = fs.readFileSync(path.join(backendDir, 'layout_preview.py'), 'utf8');
|
||||||
|
assert(
|
||||||
|
layoutPreviewPy.includes('read_gds') || layoutPreviewPy.includes('load_gds'),
|
||||||
|
'layout_preview.py should load public _BB.gds geometry, not draw only schematic boxes'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'),
|
||||||
|
'layout_preview.py should resolve public GDS assets for placed components'
|
||||||
|
);
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('Build GDS'),
|
||||||
|
'Project Tree header should include a Build GDS button'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('/api/build-gds'),
|
||||||
|
'Build GDS button should call the backend build-gds API'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes(':layout'),
|
||||||
|
'Build Layout should open an SVG preview tab named like canvas_1:layout'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('svg_url'),
|
||||||
|
'Build Layout should use the backend svg_url response'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('layoutPreview'),
|
||||||
|
'canvas pages should support a layoutPreview tab type'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('LayoutSvgPreview'),
|
||||||
|
'layout preview tabs should use the auto-scaling SVG viewer'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('layoutScale'),
|
||||||
|
'layout SVG preview should expose an editable scale value'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('objectFit: \'contain\''),
|
||||||
|
'100% layout preview scale should fit the full SVG within the screen'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('className="build-gds-btn"'),
|
||||||
|
'Build GDS should use a dedicated polished button class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('buildGdsBusy'),
|
||||||
|
'Build GDS should expose an in-progress state to prevent duplicate requests'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('Build GDS network error'),
|
||||||
|
'Build GDS fetch failures should produce a specific network diagnostic'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('className="build-layout-btn"'),
|
||||||
|
'Build Layout should use the polished primary action class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('Route Editor'),
|
||||||
|
'Selecting an edge should expose a route editor'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('selectedEdge'),
|
||||||
|
'canvas should track selected edges separately from selected nodes'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('technologyManifest'),
|
||||||
|
'canvas should load the selected technology manifest'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('standard_bend'),
|
||||||
|
'route editor should offer standard_bend as a routing type'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('findSameFamilyRouteCrossing'),
|
||||||
|
'canvas should validate same-family route crossings'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user