updated with github #5
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
-6
@@ -48,6 +48,7 @@ def init_db():
|
|||||||
"created_at": "ALTER TABLE users ADD COLUMN created_at TEXT",
|
"created_at": "ALTER TABLE users ADD COLUMN created_at TEXT",
|
||||||
"credits": "ALTER TABLE users ADD COLUMN credits INTEGER NOT NULL DEFAULT 0",
|
"credits": "ALTER TABLE users ADD COLUMN credits INTEGER NOT NULL DEFAULT 0",
|
||||||
"occupation": "ALTER TABLE users ADD COLUMN occupation TEXT NOT NULL DEFAULT 'intern'",
|
"occupation": "ALTER TABLE users ADD COLUMN occupation TEXT NOT NULL DEFAULT 'intern'",
|
||||||
|
"user_group": "ALTER TABLE users ADD COLUMN user_group TEXT NOT NULL DEFAULT 'user'",
|
||||||
}
|
}
|
||||||
for column, statement in migrations.items():
|
for column, statement in migrations.items():
|
||||||
if column not in existing_columns:
|
if column not in existing_columns:
|
||||||
@@ -55,14 +56,17 @@ def init_db():
|
|||||||
|
|
||||||
now = datetime.utcnow().strftime("%Y-%m-%d")
|
now = datetime.utcnow().strftime("%Y-%m-%d")
|
||||||
cursor.execute("UPDATE users SET created_at = ? WHERE created_at IS NULL OR created_at = ''", (now,))
|
cursor.execute("UPDATE users SET created_at = ? WHERE created_at IS NULL OR created_at = ''", (now,))
|
||||||
|
cursor.execute("UPDATE users SET user_group = 'manager' WHERE username = 'admin'")
|
||||||
|
cursor.execute("UPDATE users SET user_group = 'developers' WHERE username = 'engineer'")
|
||||||
|
cursor.execute("UPDATE users SET user_group = 'user' WHERE user_group IS NULL OR user_group = ''")
|
||||||
|
|
||||||
# Insert default users for local multi-account development.
|
# Insert default users for local multi-account development.
|
||||||
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
|
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
test_hash = generate_password_hash("123456")
|
test_hash = generate_password_hash("123456")
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO users (username, password_hash, created_at, credits, occupation) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
("admin", test_hash, now, 0, "principle engineer")
|
("admin", test_hash, now, 0, "principle engineer", "manager")
|
||||||
)
|
)
|
||||||
print("Test user created. Username: admin | Password: 123456")
|
print("Test user created. Username: admin | Password: 123456")
|
||||||
|
|
||||||
@@ -70,8 +74,8 @@ def init_db():
|
|||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
test_hash = generate_password_hash("123456")
|
test_hash = generate_password_hash("123456")
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO users (username, password_hash, created_at, credits, occupation) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, password_hash, created_at, credits, occupation, user_group) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
("engineer", test_hash, now, 0, "junior engineer")
|
("engineer", test_hash, now, 0, "junior engineer", "developers")
|
||||||
)
|
)
|
||||||
print("Second test user created. Username: engineer | Password: 123456")
|
print("Second test user created. Username: engineer | Password: 123456")
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ def init_db():
|
|||||||
def get_user(username):
|
def get_user(username):
|
||||||
conn = connect_db()
|
conn = connect_db()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT id, username, password_hash FROM users WHERE username = ?", (username,))
|
cursor.execute("SELECT id, username, password_hash, user_group FROM users WHERE username = ?", (username,))
|
||||||
user = cursor.fetchone()
|
user = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
return user
|
return user
|
||||||
@@ -90,7 +94,7 @@ def get_user_profile(user_id):
|
|||||||
conn = connect_db()
|
conn = connect_db()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id, username, created_at, credits, occupation FROM users WHERE id = ?",
|
"SELECT id, username, created_at, credits, occupation, user_group FROM users WHERE id = ?",
|
||||||
(user_id,)
|
(user_id,)
|
||||||
)
|
)
|
||||||
user = cursor.fetchone()
|
user = cursor.fetchone()
|
||||||
|
|||||||
+59
-2
@@ -1,5 +1,6 @@
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
@@ -16,13 +17,34 @@ class BuildResult:
|
|||||||
warnings: 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:
|
def build_project_gds(
|
||||||
|
project_dir: str,
|
||||||
|
output_path: str,
|
||||||
|
pdk_public_root: str,
|
||||||
|
technology_manifest_path: str = None,
|
||||||
|
prefer_full_gds: bool = False,
|
||||||
|
) -> BuildResult:
|
||||||
"""Build a hierarchical project GDS from saved cell YAML files."""
|
"""Build a hierarchical project GDS from saved cell YAML files."""
|
||||||
cells = _load_project_cells(project_dir)
|
cells = _load_project_cells(project_dir)
|
||||||
if not cells:
|
if not cells:
|
||||||
raise ValueError("No saved cell YAML files found for this project")
|
raise ValueError("No saved cell YAML files found for this project")
|
||||||
|
|
||||||
registry = PdkRegistry(pdk_public_root)
|
try:
|
||||||
|
return _build_with_mxpic_router(
|
||||||
|
project_dir,
|
||||||
|
output_path,
|
||||||
|
pdk_public_root,
|
||||||
|
technology_manifest_path,
|
||||||
|
prefer_full_gds,
|
||||||
|
)
|
||||||
|
except ImportError as router_error:
|
||||||
|
if _cells_have_links(cells):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Routed Build GDS requires mxpic_router, nazca, and mxpic_forge when layout links are present. "
|
||||||
|
f"Router import failed: {router_error}"
|
||||||
|
) from router_error
|
||||||
|
|
||||||
|
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
|
||||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -37,6 +59,33 @@ def build_project_gds(project_dir: str, output_path: str, pdk_public_root: str)
|
|||||||
) from nazca_error
|
) from nazca_error
|
||||||
|
|
||||||
|
|
||||||
|
def _build_with_mxpic_router(
|
||||||
|
project_dir: str,
|
||||||
|
output_path: str,
|
||||||
|
pdk_root: str,
|
||||||
|
technology_manifest_path: str,
|
||||||
|
prefer_full_gds: bool,
|
||||||
|
) -> BuildResult:
|
||||||
|
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
|
||||||
|
if os.path.isdir(router_root) and router_root not in sys.path:
|
||||||
|
sys.path.insert(0, router_root)
|
||||||
|
from mxpic_router import build_project_gds as build_routed_project_gds
|
||||||
|
|
||||||
|
result = build_routed_project_gds(
|
||||||
|
project_dir=project_dir,
|
||||||
|
output_path=output_path,
|
||||||
|
pdk_root=pdk_root,
|
||||||
|
technology_manifest_path=technology_manifest_path,
|
||||||
|
prefer_full_gds=prefer_full_gds,
|
||||||
|
)
|
||||||
|
return BuildResult(
|
||||||
|
output_path=result.get("output_path", output_path),
|
||||||
|
engine=result.get("engine", "mxpic_router"),
|
||||||
|
cells_built=result.get("cells_built", []),
|
||||||
|
warnings=result.get("warnings", []),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
|
def _load_project_cells(project_dir: str) -> Dict[str, dict]:
|
||||||
cells = {}
|
cells = {}
|
||||||
for filename in sorted(os.listdir(project_dir)):
|
for filename in sorted(os.listdir(project_dir)):
|
||||||
@@ -56,6 +105,14 @@ def _ordered_cell_names(cells: Dict[str, dict]) -> List[str]:
|
|||||||
return composites + projects
|
return composites + projects
|
||||||
|
|
||||||
|
|
||||||
|
def _cells_have_links(cells: Dict[str, dict]) -> bool:
|
||||||
|
for data in cells.values():
|
||||||
|
for bundle in (data.get("bundles") or {}).values():
|
||||||
|
if bundle.get("links"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||||
import gdstk
|
import gdstk
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
MANAGER_GROUP = "manager"
|
||||||
|
DEVELOPER_GROUP = "developers"
|
||||||
|
USER_GROUP = "user"
|
||||||
|
ALLOWED_GROUPS = {MANAGER_GROUP, DEVELOPER_GROUP, USER_GROUP}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_user_group(user_group: str) -> str:
|
||||||
|
group = (user_group or USER_GROUP).strip().lower()
|
||||||
|
return group if group in ALLOWED_GROUPS else USER_GROUP
|
||||||
|
|
||||||
|
|
||||||
|
def pdk_root_for_group(user_group: str, repo_root: str) -> str:
|
||||||
|
group = normalize_user_group(user_group)
|
||||||
|
if group == MANAGER_GROUP:
|
||||||
|
return os.path.abspath(os.environ.get(
|
||||||
|
"MXPIC_PDK_ATLAS_ROOT",
|
||||||
|
os.path.join(repo_root, "opt_pdk_atlas", "foundries"),
|
||||||
|
))
|
||||||
|
return os.path.abspath(os.environ.get(
|
||||||
|
"MXPIC_PDK_PUBLIC_ROOT",
|
||||||
|
os.path.join(repo_root, "opt_pdk_public", "foundries"),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def pdk_root_for_session(session_obj, repo_root: str) -> str:
|
||||||
|
return pdk_root_for_group(session_obj.get("user_group"), repo_root)
|
||||||
|
|
||||||
|
|
||||||
|
def prefer_full_gds_for_session(session_obj) -> bool:
|
||||||
|
return normalize_user_group(session_obj.get("user_group")) == MANAGER_GROUP
|
||||||
|
|
||||||
|
|
||||||
|
def create_export_path(export_root: str, project_name: str) -> tuple[str, str, str]:
|
||||||
|
export_id = uuid.uuid4().hex
|
||||||
|
filename = f"{project_name}.gds"
|
||||||
|
export_dir = os.path.abspath(os.path.join(export_root, export_id))
|
||||||
|
os.makedirs(export_dir, exist_ok=True)
|
||||||
|
return export_id, filename, os.path.join(export_dir, filename)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_expired_exports(export_root: str, max_age_seconds: int = 86400) -> None:
|
||||||
|
if not os.path.isdir(export_root):
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
for name in os.listdir(export_root):
|
||||||
|
path = os.path.join(export_root, name)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
age = now - os.path.getmtime(path)
|
||||||
|
if age > max_age_seconds:
|
||||||
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
+18
-11
@@ -16,8 +16,9 @@ class PdkAsset:
|
|||||||
class PdkRegistry:
|
class PdkRegistry:
|
||||||
"""Resolve public PDK component names to metadata and public GDS assets."""
|
"""Resolve public PDK component names to metadata and public GDS assets."""
|
||||||
|
|
||||||
def __init__(self, public_root: str):
|
def __init__(self, public_root: str, prefer_full_gds: bool = False):
|
||||||
self.public_root = os.path.abspath(public_root)
|
self.public_root = os.path.abspath(public_root)
|
||||||
|
self.prefer_full_gds = prefer_full_gds
|
||||||
self._asset_cache = {}
|
self._asset_cache = {}
|
||||||
|
|
||||||
def resolve(self, component: str) -> PdkAsset:
|
def resolve(self, component: str) -> PdkAsset:
|
||||||
@@ -60,20 +61,26 @@ class PdkRegistry:
|
|||||||
def _find_gds(self, key: str, yaml_path: Optional[str]) -> Optional[str]:
|
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("/"))
|
search_dir = os.path.dirname(yaml_path) if yaml_path else os.path.join(self.public_root, *key.split("/"))
|
||||||
name = key.split("/")[-1]
|
name = key.split("/")[-1]
|
||||||
candidates = [
|
if self.prefer_full_gds:
|
||||||
os.path.join(search_dir, f"{name}_BB.gds"),
|
candidates = [
|
||||||
os.path.join(search_dir, f"{name}.gds"),
|
os.path.join(search_dir, f"{name}.gds"),
|
||||||
]
|
os.path.join(search_dir, f"{name}_BB.gds"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
candidates = [
|
||||||
|
os.path.join(search_dir, f"{name}_BB.gds"),
|
||||||
|
os.path.join(search_dir, f"{name}.gds"),
|
||||||
|
]
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if self._inside_root(candidate) and os.path.exists(candidate):
|
if self._inside_root(candidate) and os.path.exists(candidate):
|
||||||
return os.path.abspath(candidate)
|
return os.path.abspath(candidate)
|
||||||
if os.path.isdir(search_dir):
|
if os.path.isdir(search_dir):
|
||||||
for filename in sorted(os.listdir(search_dir)):
|
gds_files = sorted(filename for filename in os.listdir(search_dir) if filename.lower().endswith(".gds"))
|
||||||
if filename.lower().endswith("_bb.gds"):
|
full_files = [filename for filename in gds_files if not filename.lower().endswith("_bb.gds")]
|
||||||
return os.path.join(search_dir, filename)
|
bb_files = [filename for filename in gds_files if filename.lower().endswith("_bb.gds")]
|
||||||
for filename in sorted(os.listdir(search_dir)):
|
ordered = (full_files + bb_files) if self.prefer_full_gds else (bb_files + full_files)
|
||||||
if filename.lower().endswith(".gds"):
|
if ordered:
|
||||||
return os.path.join(search_dir, filename)
|
return os.path.join(search_dir, ordered[0])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
|
def _load_yaml(self, yaml_path: Optional[str]) -> Optional[dict]:
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def create_routed_layout_svg(
|
||||||
|
yaml_content: str,
|
||||||
|
output_path: str,
|
||||||
|
pdk_root: str,
|
||||||
|
project_dir: str,
|
||||||
|
technology_manifest_path: str = None,
|
||||||
|
prefer_full_gds: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Create an SVG preview from routed GDS geometry generated by mxpic_router."""
|
||||||
|
import gdstk
|
||||||
|
|
||||||
|
layout = yaml.safe_load(yaml_content) or {}
|
||||||
|
cell_name = str(layout.get("name") or "layout")
|
||||||
|
router_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_router"))
|
||||||
|
if os.path.isdir(router_root) and router_root not in sys.path:
|
||||||
|
sys.path.insert(0, router_root)
|
||||||
|
from mxpic_router import build_project_gds
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mxpic_routed_preview_") as temp_dir:
|
||||||
|
temp_gds = os.path.join(temp_dir, f"{cell_name}.gds")
|
||||||
|
build_project_gds(
|
||||||
|
project_dir=project_dir,
|
||||||
|
output_path=temp_gds,
|
||||||
|
pdk_root=pdk_root,
|
||||||
|
technology_manifest_path=technology_manifest_path,
|
||||||
|
prefer_full_gds=prefer_full_gds,
|
||||||
|
target_cell_name=cell_name,
|
||||||
|
)
|
||||||
|
library = gdstk.read_gds(temp_gds)
|
||||||
|
top_cells = library.top_level()
|
||||||
|
if not top_cells:
|
||||||
|
raise RuntimeError("Routed preview GDS has no top-level cell")
|
||||||
|
top_cells[0].write_svg(output_path)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def layout_has_links(yaml_content: str) -> bool:
|
||||||
|
layout = yaml.safe_load(yaml_content) or {}
|
||||||
|
for bundle in (layout.get("bundles") or {}).values():
|
||||||
|
links = bundle.get("links") or []
|
||||||
|
if links:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
+81
-13
@@ -13,6 +13,13 @@ from flask import Response
|
|||||||
from gds_builder import build_project_gds
|
from gds_builder import build_project_gds
|
||||||
from layout_preview import create_layout_svg_from_gds
|
from layout_preview import create_layout_svg_from_gds
|
||||||
from pdk_registry import PdkRegistry
|
from pdk_registry import PdkRegistry
|
||||||
|
from pdk_access import (
|
||||||
|
cleanup_expired_exports,
|
||||||
|
create_export_path,
|
||||||
|
pdk_root_for_session,
|
||||||
|
prefer_full_gds_for_session,
|
||||||
|
)
|
||||||
|
from routed_layout_preview import create_routed_layout_svg, layout_has_links
|
||||||
from technology_manifest import TechnologyManifestError, read_technology_manifest
|
from technology_manifest import TechnologyManifestError, read_technology_manifest
|
||||||
|
|
||||||
# --- Path Configurations ---
|
# --- Path Configurations ---
|
||||||
@@ -26,13 +33,12 @@ PDK_PUBLIC_ROOT = os.path.abspath(os.environ.get(
|
|||||||
))
|
))
|
||||||
EDA_PDK_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs'))
|
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.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)
|
EXPORT_ROOT = os.path.abspath(os.path.join(DATABASE_ROOT, '_exports'))
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
|
app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR)
|
||||||
@@ -120,6 +126,25 @@ def project_gds_path(project_name):
|
|||||||
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
|
return os.path.join(project_root(project_name), f"{safe_name(project_name, 'project_1')}.gds")
|
||||||
|
|
||||||
|
|
||||||
|
def technology_manifest_path_for_project(project_name):
|
||||||
|
technology_id = read_project_meta(project_name).get("technology") or ""
|
||||||
|
if "/" not in technology_id:
|
||||||
|
return None
|
||||||
|
foundry, technology = technology_id.split("/", 1)
|
||||||
|
path = os.path.abspath(os.path.join(EDA_PDK_ROOT, safe_name(foundry, ''), safe_name(technology, ''), "technology.yml"))
|
||||||
|
if path.startswith(EDA_PDK_ROOT + os.sep) and os.path.exists(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def current_pdk_root():
|
||||||
|
return pdk_root_for_session(session, REPO_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def current_pdk_registry():
|
||||||
|
return PdkRegistry(current_pdk_root(), prefer_full_gds=prefer_full_gds_for_session(session))
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
@@ -220,9 +245,10 @@ def getIcon(category):
|
|||||||
|
|
||||||
# ... [Keep existing API routes below] ...
|
# ... [Keep existing API routes below] ...
|
||||||
|
|
||||||
def readCompYaml(compName):
|
def readCompYaml(compName, comps_root=None):
|
||||||
"""Load YAML from component folder."""
|
"""Load YAML from component folder."""
|
||||||
for root, dirs, files in os.walk(COMPS_ROOT):
|
search_root = comps_root or current_pdk_root()
|
||||||
|
for root, dirs, files in os.walk(search_root):
|
||||||
if os.path.basename(root) == compName:
|
if os.path.basename(root) == compName:
|
||||||
dirs.clear()
|
dirs.clear()
|
||||||
ymlFiles = [f for f in files if f.endswith('.yml')]
|
ymlFiles = [f for f in files if f.endswith('.yml')]
|
||||||
@@ -252,6 +278,7 @@ def login():
|
|||||||
if user and check_password_hash(user[2], password):
|
if user and check_password_hash(user[2], password):
|
||||||
session['user_id'] = user[0]
|
session['user_id'] = user[0]
|
||||||
session['username'] = user[1]
|
session['username'] = user[1]
|
||||||
|
session['user_group'] = user[3] or 'user'
|
||||||
record_action('login')
|
record_action('login')
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
else:
|
else:
|
||||||
@@ -355,6 +382,7 @@ def account_profile():
|
|||||||
"created_at": profile[2],
|
"created_at": profile[2],
|
||||||
"credits": profile[3] or 0,
|
"credits": profile[3] or 0,
|
||||||
"occupation": profile[4] or "intern",
|
"occupation": profile[4] or "intern",
|
||||||
|
"user_group": profile[5] or "user",
|
||||||
"occupations": sorted(occupations)
|
"occupations": sorted(occupations)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -568,7 +596,17 @@ def save_layout():
|
|||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
svg_path = cell_svg_path(project, cell)
|
svg_path = cell_svg_path(project, cell)
|
||||||
create_layout_svg_from_gds(content, svg_path, pdk_registry=PDK_REGISTRY, project_dir=project_root(project))
|
if layout_has_links(content):
|
||||||
|
create_routed_layout_svg(
|
||||||
|
content,
|
||||||
|
svg_path,
|
||||||
|
pdk_root=current_pdk_root(),
|
||||||
|
project_dir=project_root(project),
|
||||||
|
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||||
|
prefer_full_gds=prefer_full_gds_for_session(session),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
create_layout_svg_from_gds(content, svg_path, pdk_registry=current_pdk_registry(), project_dir=project_root(project))
|
||||||
|
|
||||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -605,11 +643,18 @@ def build_gds():
|
|||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
project = safe_name(data.get('project'), 'project_1')
|
project = safe_name(data.get('project'), 'project_1')
|
||||||
try:
|
try:
|
||||||
|
cleanup_expired_exports(EXPORT_ROOT)
|
||||||
project_dir = ensure_project_path(project)
|
project_dir = ensure_project_path(project)
|
||||||
if not os.path.isdir(project_dir):
|
if not os.path.isdir(project_dir):
|
||||||
return jsonify({"error": "Project not found"}), 404
|
return jsonify({"error": "Project not found"}), 404
|
||||||
output_path = project_gds_path(project)
|
export_id, filename, output_path = create_export_path(EXPORT_ROOT, safe_name(project, 'project_1'))
|
||||||
result = build_project_gds(project_dir, output_path, PDK_PUBLIC_ROOT)
|
result = build_project_gds(
|
||||||
|
project_dir,
|
||||||
|
output_path,
|
||||||
|
current_pdk_root(),
|
||||||
|
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||||
|
prefer_full_gds=prefer_full_gds_for_session(session),
|
||||||
|
)
|
||||||
record_action('gds.build', project=project, detail={
|
record_action('gds.build', project=project, detail={
|
||||||
"path": result.output_path,
|
"path": result.output_path,
|
||||||
"engine": result.engine,
|
"engine": result.engine,
|
||||||
@@ -619,7 +664,8 @@ def build_gds():
|
|||||||
"message": "GDS built",
|
"message": "GDS built",
|
||||||
"project": project,
|
"project": project,
|
||||||
"path": result.output_path,
|
"path": result.output_path,
|
||||||
"gds_url": url_for('get_project_gds', project_name=project, filename=os.path.basename(result.output_path)),
|
"filename": filename,
|
||||||
|
"download_url": url_for('download_export', export_id=export_id, filename=filename),
|
||||||
"engine": result.engine,
|
"engine": result.engine,
|
||||||
"cells_built": result.cells_built,
|
"cells_built": result.cells_built,
|
||||||
"warnings": result.warnings
|
"warnings": result.warnings
|
||||||
@@ -628,6 +674,24 @@ def build_gds():
|
|||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/exports/<export_id>/<filename>')
|
||||||
|
@login_required_json
|
||||||
|
def download_export(export_id, filename):
|
||||||
|
export_id = safe_name(export_id, '')
|
||||||
|
safe_filename = safe_name(filename, 'layout.gds')
|
||||||
|
export_dir = os.path.abspath(os.path.join(EXPORT_ROOT, export_id))
|
||||||
|
if not export_id or not export_dir.startswith(EXPORT_ROOT + os.sep):
|
||||||
|
return jsonify({"error": "Invalid export path"}), 400
|
||||||
|
if not safe_filename.lower().endswith('.gds'):
|
||||||
|
return jsonify({"error": "Invalid GDS filename"}), 400
|
||||||
|
gds_path = os.path.abspath(os.path.join(export_dir, safe_filename))
|
||||||
|
if not gds_path.startswith(export_dir + os.sep) or not os.path.exists(gds_path):
|
||||||
|
return jsonify({"error": "GDS export not found"}), 404
|
||||||
|
response = send_from_directory(export_dir, safe_filename, as_attachment=True)
|
||||||
|
response.call_on_close(lambda: shutil.rmtree(export_dir, ignore_errors=True))
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/projects/<project_name>/gds/<filename>')
|
@app.route('/api/projects/<project_name>/gds/<filename>')
|
||||||
@login_required_json
|
@login_required_json
|
||||||
def get_project_gds(project_name, filename):
|
def get_project_gds(project_name, filename):
|
||||||
@@ -649,28 +713,32 @@ def get_project_gds(project_name, filename):
|
|||||||
|
|
||||||
# --- API ROUTES (Library & Components) ---
|
# --- API ROUTES (Library & Components) ---
|
||||||
@app.route('/api/library')
|
@app.route('/api/library')
|
||||||
|
@login_required_json
|
||||||
def getLib():
|
def getLib():
|
||||||
"""Get library structure."""
|
"""Get library structure."""
|
||||||
# tree = buildTree(YML_PATH)
|
comps_root = current_pdk_root()
|
||||||
if os.path.isdir(COMPS_ROOT):
|
fresh_tree = {}
|
||||||
compMap = findComps(COMPS_ROOT)
|
if os.path.isdir(comps_root):
|
||||||
|
compMap = findComps(comps_root)
|
||||||
fresh_tree = addCompsToTree(compMap)
|
fresh_tree = addCompsToTree(compMap)
|
||||||
return jsonify(fresh_tree)
|
return jsonify(fresh_tree)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/component/<component_name>')
|
@app.route('/api/component/<component_name>')
|
||||||
|
@login_required_json
|
||||||
def getComp(component_name):
|
def getComp(component_name):
|
||||||
"""Return component YAML data."""
|
"""Return component YAML data."""
|
||||||
data = readCompYaml(component_name)
|
data = readCompYaml(component_name, current_pdk_root())
|
||||||
if data is None:
|
if data is None:
|
||||||
return jsonify({"error": "Component not found"}), 404
|
return jsonify({"error": "Component not found"}), 404
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
@app.route('/api/component/<component_name>/image')
|
@app.route('/api/component/<component_name>/image')
|
||||||
|
@login_required_json
|
||||||
def getCompImg(component_name):
|
def getCompImg(component_name):
|
||||||
"""Return first image in component folder."""
|
"""Return first image in component folder."""
|
||||||
for root, dirs, files in os.walk(COMPS_ROOT):
|
for root, dirs, files in os.walk(current_pdk_root()):
|
||||||
if os.path.basename(root) == component_name:
|
if os.path.basename(root) == component_name:
|
||||||
dirs.clear()
|
dirs.clear()
|
||||||
for ext in ('.png', '.jpg', '.jpeg', '.svg'):
|
for ext in ('.png', '.jpg', '.jpeg', '.svg'):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,41 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,61 +0,0 @@
|
|||||||
# =============================================
|
|
||||||
# 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.
@@ -1,45 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -9,34 +9,69 @@ 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
|
||||||
|
layer: WG_CORE
|
||||||
|
x: 50.0
|
||||||
|
y: 150.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:
|
||||||
canvas_1:
|
component_1:
|
||||||
component: canvas_1
|
|
||||||
x: 390.0
|
|
||||||
y: 290.0
|
|
||||||
rotation: 0.0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
component_7:
|
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/2x2MMI_1310nm_TE_Silterra_202603_ZKY
|
||||||
x: 840.0
|
x: 300.0
|
||||||
y: 290.0
|
y: 440.0
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
mirror: false
|
mirror: false
|
||||||
settings:
|
settings:
|
||||||
length:
|
length:
|
||||||
|
|
||||||
elements: {}
|
component_2:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303
|
||||||
|
x: 820.0
|
||||||
|
y: 250.0
|
||||||
|
rotation: 0.0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
component_3:
|
||||||
|
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303
|
||||||
|
x: 820.0
|
||||||
|
y: 660.0
|
||||||
|
rotation: 0.0
|
||||||
|
mirror: false
|
||||||
|
settings:
|
||||||
|
length:
|
||||||
|
|
||||||
|
elements:
|
||||||
|
port:
|
||||||
|
type: port
|
||||||
|
x: 50.0
|
||||||
|
y: 150.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: component_7:a1
|
- from: component_2:g2b
|
||||||
to: canvas_1:port
|
to: component_1:b1
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
|
- from: component_1:b2
|
||||||
|
to: component_3:g2b
|
||||||
|
xsection: strip
|
||||||
|
family: optical
|
||||||
|
width: 0.45
|
||||||
|
radius: 10
|
||||||
|
routing_type: euler_bend
|
||||||
Binary file not shown.
+10
-1
@@ -3948,7 +3948,16 @@ ${bundlesBlock}`;
|
|||||||
const warningText = result.warnings && result.warnings.length > 0
|
const warningText = result.warnings && result.warnings.length > 0
|
||||||
? ` (${result.warnings.length} warnings)`
|
? ` (${result.warnings.length} warnings)`
|
||||||
: '';
|
: '';
|
||||||
addLog(`GDS built with ${result.engine}: ${result.path}${warningText}`);
|
if (result.download_url) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = result.download_url;
|
||||||
|
link.download = result.filename || `${currentProjectName}.gds`;
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
addLog(`GDS built with ${result.engine}: ${result.filename || result.path}${warningText}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addLog(`Build GDS network error: ${err.message}. Check that the Flask server is running from the same host and Python environment.`);
|
addLog(`Build GDS network error: ${err.message}. Check that the Flask server is running from the same host and Python environment.`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -608,6 +608,10 @@
|
|||||||
<label>Credits</label>
|
<label>Credits</label>
|
||||||
<div class="profile-value" id="profile-credits">0</div>
|
<div class="profile-value" id="profile-credits">0</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="profile-item">
|
||||||
|
<label>User Group</label>
|
||||||
|
<div class="profile-value" id="profile-group">-</div>
|
||||||
|
</div>
|
||||||
<div class="profile-item">
|
<div class="profile-item">
|
||||||
<label>Occupation</label>
|
<label>Occupation</label>
|
||||||
<select id="profile-occupation"></select>
|
<select id="profile-occupation"></select>
|
||||||
@@ -694,6 +698,7 @@
|
|||||||
const profileUsername = document.getElementById('profile-username');
|
const profileUsername = document.getElementById('profile-username');
|
||||||
const profileCreated = document.getElementById('profile-created');
|
const profileCreated = document.getElementById('profile-created');
|
||||||
const profileCredits = document.getElementById('profile-credits');
|
const profileCredits = document.getElementById('profile-credits');
|
||||||
|
const profileGroup = document.getElementById('profile-group');
|
||||||
const profileOccupation = document.getElementById('profile-occupation');
|
const profileOccupation = document.getElementById('profile-occupation');
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
const logTerminal = document.getElementById('log-terminal');
|
const logTerminal = document.getElementById('log-terminal');
|
||||||
@@ -733,6 +738,7 @@
|
|||||||
profileUsername.textContent = profile.username;
|
profileUsername.textContent = profile.username;
|
||||||
profileCreated.textContent = profile.created_at || '-';
|
profileCreated.textContent = profile.created_at || '-';
|
||||||
profileCredits.textContent = profile.credits ?? 0;
|
profileCredits.textContent = profile.credits ?? 0;
|
||||||
|
profileGroup.textContent = profile.user_group || 'user';
|
||||||
profileOccupation.innerHTML = '';
|
profileOccupation.innerHTML = '';
|
||||||
(profile.occupations || []).forEach(occupation => {
|
(profile.occupations || []).forEach(occupation => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ layers:
|
|||||||
WG_HM: {layer: 275, datatype: 0}
|
WG_HM: {layer: 275, datatype: 0}
|
||||||
WG_STRIP: {layer: 101, datatype: 251}
|
WG_STRIP: {layer: 101, datatype: 251}
|
||||||
WG_LOWRIB: {layer: 100, datatype: 90}
|
WG_LOWRIB: {layer: 100, datatype: 90}
|
||||||
|
WG_SRIB: {layer: 100, datatype: 90}
|
||||||
WG_HIGHRIB: {layer: 232, datatype: 0}
|
WG_HIGHRIB: {layer: 232, datatype: 0}
|
||||||
HEATER: {layer: 29, datatype: 30}
|
HEATER: {layer: 29, datatype: 30}
|
||||||
CT_SI: {layer: 268, datatype: 0}
|
CT_SI: {layer: 268, datatype: 0}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const backend = path.join(root, 'backend');
|
||||||
|
const databasePy = fs.readFileSync(path.join(backend, 'database.py'), 'utf8');
|
||||||
|
const serverPy = fs.readFileSync(path.join(backend, 'server.py'), 'utf8');
|
||||||
|
const dashboardHtml = fs.readFileSync(path.join(root, 'frontend', 'dashboard.html'), 'utf8');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
databasePy.includes('user_group'),
|
||||||
|
'database migration should add users.user_group'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
databasePy.includes("'admin'") && databasePy.includes("'manager'"),
|
||||||
|
'admin should be migrated/seeded as manager'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
databasePy.includes("'engineer'") && databasePy.includes("'developers'"),
|
||||||
|
'engineer should be migrated/seeded as developers'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes("session['user_group']") || serverPy.includes('session["user_group"]'),
|
||||||
|
'login should store user_group in the session'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('"user_group"'),
|
||||||
|
'/api/profile should return user_group'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backend, 'pdk_access.py')),
|
||||||
|
'backend/pdk_access.py should resolve role-based PDK roots'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('pdk_root_for_session'),
|
||||||
|
'server should resolve PDK root per logged-in user group'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
dashboardHtml.includes('profile-group'),
|
||||||
|
'dashboard should show read-only profile group information'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!dashboardHtml.includes('profile-group"></select'),
|
||||||
|
'profile group must not be editable'
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const serverPy = fs.readFileSync(path.join(root, 'backend', 'server.py'), 'utf8');
|
||||||
|
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
serverPy.includes('EXPORT_ROOT'),
|
||||||
|
'server should build GDS exports into a temporary export root'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes("@app.route('/api/exports/<export_id>/<filename>'"),
|
||||||
|
'server should expose an authenticated export download route'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('download_url'),
|
||||||
|
'Build GDS response should include download_url'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('cleanup_expired_exports'),
|
||||||
|
'server should clean exports older than the retention period'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('download_url'),
|
||||||
|
'frontend should read download_url from Build GDS response'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('document.createElement') && canvasHtml.includes('.download'),
|
||||||
|
'frontend should trigger a browser download for generated GDS'
|
||||||
|
);
|
||||||
@@ -18,10 +18,18 @@ assert(
|
|||||||
fs.existsSync(path.join(backendDir, 'gds_builder.py')),
|
fs.existsSync(path.join(backendDir, 'gds_builder.py')),
|
||||||
'backend/gds_builder.py should build hierarchical GDS from saved project YAML'
|
'backend/gds_builder.py should build hierarchical GDS from saved project YAML'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
fs.existsSync(path.join(backendDir, 'routed_layout_preview.py')),
|
||||||
|
'backend/routed_layout_preview.py should create routed SVG previews through mxpic_router'
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
serverPy.includes('create_layout_svg_from_gds'),
|
serverPy.includes('create_layout_svg_from_gds'),
|
||||||
'save-layout route should create a GDS-derived layout SVG preview'
|
'save-layout route should create a GDS-derived layout SVG preview'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
serverPy.includes('create_routed_layout_svg'),
|
||||||
|
'save-layout route should use routed preview generation when links exist'
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
serverPy.includes('svg_url'),
|
serverPy.includes('svg_url'),
|
||||||
'save-layout response should include an svg_url for the new layout tab'
|
'save-layout response should include an svg_url for the new layout tab'
|
||||||
@@ -64,3 +72,9 @@ assert(
|
|||||||
layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'),
|
layoutPreviewPy.includes('_BB.gds') || layoutPreviewPy.includes('gds_path'),
|
||||||
'layout_preview.py should resolve public GDS assets for placed components'
|
'layout_preview.py should resolve public GDS assets for placed components'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gdsBuilderPy = fs.readFileSync(path.join(backendDir, 'gds_builder.py'), 'utf8');
|
||||||
|
assert(
|
||||||
|
gdsBuilderPy.includes('_cells_have_links') && gdsBuilderPy.includes('Routed Build GDS requires mxpic_router'),
|
||||||
|
'Build GDS should not silently fall back to unrouted gdstk when links are present'
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user