diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc deleted file mode 100644 index 45b6547..0000000 Binary files a/backend/__pycache__/database.cpython-39.pyc and /dev/null differ diff --git a/backend/database.py b/backend/database.py index 3a5eafd..42941aa 100644 --- a/backend/database.py +++ b/backend/database.py @@ -4,7 +4,7 @@ import os from werkzeug.security import generate_password_hash # Save the database in the backend folder -DB_FILE = os.path.join(os.path.dirname(__file__), "..\\database\\mxpic_data.db") +DB_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "database", "mxpic_data.db")) def init_db(): conn = sqlite3.connect(DB_FILE) @@ -39,4 +39,4 @@ def get_user(username): if __name__ == "__main__": init_db() - print("Database initialized successfully.") \ No newline at end of file + print("Database initialized successfully.") diff --git a/backend/dir_test.py b/backend/dir_test.py deleted file mode 100644 index 2980d50..0000000 --- a/backend/dir_test.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import yaml -from collections import OrderedDict -from flask import Flask, jsonify, send_from_directory, request, redirect, url_for, session, render_template -from werkzeug.security import check_password_hash -import database - -# --- Path Configurations --- -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend') - -YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml') -COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra') -# Define where your new icons folder is located (adjust if it's placed elsewhere) -ICONS_DIR = os.path.join(BASE_DIR, 'icons') - -app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR) -app.secret_key = 'super_secret_mxpic_key' -app.json.sort_keys = False - -database.init_db() - -# ... [Keep countSpaces and buildTree exactly as they are] ... - -def findComps(baseDir): - """Scan component folders, return map of paths -> component info.""" - compMap = {} - refDir = baseDir - for root, dirs, files in os.walk(baseDir): - ymlFiles = [f for f in files if f.endswith('.yml')] - if ymlFiles: - parentDir = os.path.dirname(root) - relPath = os.path.relpath(parentDir, refDir) - parts = () if relPath == '.' else tuple(relPath.split(os.sep)) - compName = os.path.basename(root) - - # Extract the category (the mother folder's name) - category = os.path.basename(parentDir) - - compMap[parts] = { - 'folder': compName, - 'yml': ymlFiles[0], - 'category': category # Save the category to the map - } - dirs.clear() - return compMap - -def addCompsToTree(compMap): - """Build a completely fresh tree from scratch and insert component nodes.""" - fresh_tree = OrderedDict() - - for pathSeg, compItem in compMap.items(): - compName = compItem['folder'] - curNode = fresh_tree - - for seg in pathSeg: - if seg not in curNode: - curNode[seg] = OrderedDict() - curNode = curNode[seg] - - curNode[compName] = OrderedDict({ - "__type__": "component", - "__name__": compName, - "__yml__": compItem['yml'], - "__category__": compItem['category'] # Inject category into the tree - }) - - return fresh_tree - - -if os.path.isdir(COMPS_ROOT): - compMap = findComps(COMPS_ROOT) - fresh_tree = addCompsToTree(compMap) - -print(compMap) -print(fresh_tree) - - - diff --git a/backend/server.py b/backend/server.py index 10741f2..8db4504 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1,128 +1,90 @@ + import os +import re +import shutil +import json import yaml from collections import OrderedDict +from functools import wraps from flask import Flask, jsonify, send_from_directory, request, redirect, url_for, session, render_template from werkzeug.security import check_password_hash -import database # Imports the database.py you created earlier +import database +from flask import Response # --- Path Configurations --- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend') -# Use os.path.join exclusively for cross-platform safety YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml') COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra') +# Define where your new icons folder is located (adjust if it's placed elsewhere) +ICONS_DIR = os.path.join(BASE_DIR, 'icons') + +#build layout save path +DATABASE_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', 'database')) + -# Initialize Flask, pointing to the frontend folder for HTML/CSS/JS app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR) -app.secret_key = 'super_secret_mxpic_key' # Required for session management -app.json.sort_keys = False # Keep dictionary order +app.secret_key = 'super_secret_mxpic_key' +app.json.sort_keys = False -# Ensure database tables exist when the server boots database.init_db() -# --- YAML & PDK Parsing Helper Functions (Unchanged) --- -def countSpaces(line): - """Count leading spaces (tab=4).""" - expanded = line.expandtabs(4) - return len(expanded) - len(expanded.lstrip(' ')) -def buildTree(filepath): - """Build nested tree from indented yaml.""" - if not os.path.exists(filepath): - return OrderedDict() - - with open(filepath, 'r', encoding='utf-8') as f: - lines = f.readlines() +def login_required_json(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + if 'user_id' not in session: + return jsonify({"error": "Authentication required"}), 401 + return view_func(*args, **kwargs) + return wrapper - rootIdx = None - for i, line in enumerate(lines): - if line.strip().startswith('root') and ':' in line.strip(): - rootIdx = i - break - if rootIdx is None: - return OrderedDict() - entries = [] - for line in lines[rootIdx + 1:]: - stripped = line.strip() - if not stripped or stripped.startswith('#'): - continue - if stripped.startswith('- '): - spaceNum = countSpaces(line) - # FIX 1: Strip trailing colons off the string so 'composites:' becomes 'composites' - name = stripped[2:].strip().rstrip(':') - if name: - entries.append((spaceNum, name)) +def safe_name(value, fallback): + """Keep user/project/cell names filesystem-friendly without changing display names.""" + value = (value or '').strip() + if not value: + value = fallback + value = re.sub(r'[^A-Za-z0-9_.-]+', '_', value) + value = value.strip('._') + return value or fallback - if not entries: - return OrderedDict() - minIndent = min(indent for indent, _ in entries) - nest = OrderedDict() - levelStack = [(minIndent - 1, nest)] +def user_layout_root(): + username = safe_name(session.get('username'), 'anonymous') + return os.path.join(DATABASE_ROOT, username, 'layout') - for spaceNum, name in entries: - while levelStack and levelStack[-1][0] >= spaceNum: - levelStack.pop() - parent = levelStack[-1][1] - child = OrderedDict() - parent[name] = child - levelStack.append((spaceNum, child)) - return nest +def project_root(project_name): + return os.path.join(user_layout_root(), safe_name(project_name, 'project_1')) -# def addCompsToTree(tree, compMap): -# """Insert component nodes into the tree.""" -# for pathSeg, compItem in compMap.items(): -# compName = compItem['folder'] -# curNode = tree - -# # FIX 2: Automatically build missing folder paths -# for seg in pathSeg: -# if seg not in curNode: -# # If a folder like MZM_1600G isn't in the YAML, gracefully auto-create it -# curNode[seg] = OrderedDict() -# curNode = curNode[seg] - -# curNode[compName] = OrderedDict({ -# "__type__": "component", -# "__name__": compName, -# "__yml__": compItem['yml'] -# }) -# return tree -def addCompsToTree(compMap): - """ - Build a completely fresh tree from scratch and insert component nodes. - No previous tree object or inspection required. - """ - # Initialize a clean, empty root tree - fresh_tree = OrderedDict() - - for pathSeg, compItem in compMap.items(): - compName = compItem['folder'] - curNode = fresh_tree - - # Sequentially build the nested path segments dynamically - for seg in pathSeg: - if seg not in curNode: - curNode[seg] = OrderedDict() - curNode = curNode[seg] - - # Place the component metadata dictionary into its leaf node - curNode[compName] = OrderedDict({ - "__type__": "component", - "__name__": compName, - "__yml__": compItem['yml'] - }) - - return fresh_tree +def cell_file_path(project_name, cell_name): + return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.yml") + + +def project_meta_path(project_name): + return os.path.join(project_root(project_name), ".project.json") + + +def read_project_meta(project_name): + path = project_meta_path(project_name) + if not os.path.exists(path): + return {} + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + + +def write_project_meta(project_name, meta): + os.makedirs(project_root(project_name), exist_ok=True) + with open(project_meta_path(project_name), 'w', encoding='utf-8') as f: + json.dump(meta, f, indent=2) + +# ... [Keep countSpaces and buildTree exactly as they are] ... def findComps(baseDir): """Scan component folders, return map of paths -> component info.""" compMap = {} - # refDir = os.path.dirname(baseDir) refDir = baseDir for root, dirs, files in os.walk(baseDir): ymlFiles = [f for f in files if f.endswith('.yml')] @@ -131,29 +93,67 @@ def findComps(baseDir): relPath = os.path.relpath(parentDir, refDir) parts = () if relPath == '.' else tuple(relPath.split(os.sep)) compName = os.path.basename(root) - compMap[parts] = { + + # Extract the category (the mother folder's name) + category = os.path.basename(parentDir) + + # Include compName in the key so multiple cells in one category do not overwrite each other. + compMap[parts + (compName,)] = { 'folder': compName, - 'yml': ymlFiles[0] + 'yml': ymlFiles[0], + 'category': category # Save the category to the map } dirs.clear() return compMap -# def addCompsToTree(tree, compMap): -# """Insert component nodes into the tree.""" -# for pathSeg, compItem in compMap.items(): -# compName = compItem['folder'] -# curNode = tree -# try: -# for seg in pathSeg: -# curNode = curNode[seg] -# except KeyError: -# continue -# curNode[compName] = OrderedDict({ -# "__type__": "component", -# "__name__": compName, -# "__yml__": compItem['yml'] -# }) -# return tree +def addCompsToTree(compMap): + """Build a completely fresh tree from scratch and insert component nodes.""" + fresh_tree = OrderedDict() + + for mapKey, compItem in compMap.items(): + pathSeg = mapKey[:-1] + compName = compItem['folder'] + curNode = fresh_tree + + for seg in pathSeg: + if seg not in curNode: + curNode[seg] = OrderedDict() + curNode = curNode[seg] + + curNode[compName] = OrderedDict({ + "__type__": "component", + "__name__": compName, + "__yml__": compItem['yml'], + "__category__": compItem['category'] # Inject category into the tree + }) + + return fresh_tree + +# ... [Keep readCompYaml and Page Routes exactly as they are] ... + +# --- API ROUTES (Library, Components & Icons) --- + +@app.route('/api/icon/') +def getIcon(category): + """Serve the icon corresponding to the component category.""" + for ext in ('.png', '.svg', '.jpg'): + icon_path = os.path.join(ICONS_DIR, f"{category}{ext}") + if os.path.exists(icon_path): + return send_from_directory(ICONS_DIR, f"{category}{ext}") + + fallback = os.path.join(ICONS_DIR, "default.png") + if os.path.exists(fallback): + return send_from_directory(ICONS_DIR, "default.png") + + # return png if not found + transparent_png = ( + b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01' + b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01' + b'\x00\x00\x05\x00\x01\r\n\xf4\xc0\x00\x00\x00\x00IEND\xaeB`\x82' + ) + return Response(transparent_png, mimetype='image/png') + +# ... [Keep existing API routes below] ... def readCompYaml(compName): """Load YAML from component folder.""" @@ -214,11 +214,185 @@ def logout(): session.clear() return redirect(url_for('home')) + +@app.route('/api/technologies', methods=['GET']) +@login_required_json +def list_technologies(): + """List technology choices from mxpic/PDKs//.""" + technologies = [] + pdks_root = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs') + if os.path.isdir(pdks_root): + for foundry in sorted(os.listdir(pdks_root)): + foundry_path = os.path.join(pdks_root, foundry) + if not os.path.isdir(foundry_path): + continue + for technology in sorted(os.listdir(foundry_path)): + technology_path = os.path.join(foundry_path, technology) + if not os.path.isdir(technology_path): + continue + technologies.append({ + "foundry": foundry, + "technology": technology, + "id": f"{foundry}/{technology}", + "label": f"{foundry} / {technology}" + }) + return jsonify({"technologies": technologies}) + + +@app.route('/api/projects', methods=['GET']) +@login_required_json +def list_projects(): + """List projects stored under database//layout.""" + root = user_layout_root() + os.makedirs(root, exist_ok=True) + + projects = [] + for name in sorted(os.listdir(root)): + path = os.path.join(root, name) + if not os.path.isdir(path): + continue + cells = [] + for filename in sorted(os.listdir(path)): + if not filename.lower().endswith(('.yml', '.yaml')): + continue + cell_name = os.path.splitext(filename)[0] + yml_path = os.path.join(path, filename) + cells.append({ + "name": cell_name, + "has_layout": os.path.exists(yml_path) + }) + meta = read_project_meta(name) + projects.append({ + "name": name, + "cells": cells, + "technology": meta.get("technology") + }) + + return jsonify({"projects": projects}) + + +@app.route('/api/projects', methods=['POST']) +@login_required_json +def create_project(): + data = request.get_json(silent=True) or {} + requested_name = safe_name(data.get('name'), 'project_1') + technology = data.get('technology') or '' + root = user_layout_root() + os.makedirs(root, exist_ok=True) + + project_name = requested_name + counter = 1 + while os.path.exists(os.path.join(root, project_name)): + counter += 1 + project_name = f"{requested_name}_{counter}" + + os.makedirs(project_root(project_name), exist_ok=True) + write_project_meta(project_name, { + "name": project_name, + "technology": technology + }) + return jsonify({"name": project_name, "technology": technology}), 201 + + +@app.route('/api/projects/', methods=['GET']) +@login_required_json +def get_project(project_name): + """Load all saved cells for a project.""" + root = project_root(project_name) + if not os.path.isdir(root): + return jsonify({"error": "Project not found"}), 404 + + cells = [] + for filename in sorted(os.listdir(root)): + if not filename.lower().endswith(('.yml', '.yaml')): + continue + cell_name = os.path.splitext(filename)[0] + yml_path = os.path.join(root, filename) + if not os.path.exists(yml_path): + continue + with open(yml_path, 'r', encoding='utf-8') as f: + cells.append({ + "name": cell_name, + "content": f.read() + }) + + return jsonify({ + "name": safe_name(project_name, 'project_1'), + "cells": cells, + "technology": read_project_meta(project_name).get("technology") + }) + + +@app.route('/api/projects/', methods=['DELETE']) +@login_required_json +def delete_project(project_name): + """Delete a user's project folder under database//layout.""" + root = project_root(project_name) + layout_root = os.path.abspath(user_layout_root()) + target = os.path.abspath(root) + + if not target.startswith(layout_root + os.sep): + return jsonify({"error": "Invalid project path"}), 400 + if not os.path.isdir(target): + return jsonify({"error": "Project not found"}), 404 + + shutil.rmtree(target) + return jsonify({"message": "deleted", "project": safe_name(project_name, 'project_1')}) + + +@app.route('/api/projects//cells/', methods=['PATCH']) +@login_required_json +def rename_cell(project_name, cell_name): + data = request.get_json(silent=True) or {} + old_cell = safe_name(cell_name, 'canvas_1') + new_cell = safe_name(data.get('name'), old_cell) + if old_cell == new_cell: + return jsonify({"message": "unchanged", "cell": new_cell}) + + old_path = cell_file_path(project_name, old_cell) + new_path = cell_file_path(project_name, new_cell) + if os.path.exists(new_path): + return jsonify({"error": "Cell name already exists"}), 409 + if os.path.exists(old_path): + os.rename(old_path, new_path) + + return jsonify({"message": "renamed", "old_cell": old_cell, "cell": new_cell}) + + + + +@app.route('/api/save-layout', methods=['POST']) +@login_required_json +def save_layout(): + try: + data = request.get_json() + project = safe_name(data.get('project'), 'project_1') + cell = safe_name(data.get('cell'), 'canvas_1') + content = data.get('content', '') + + save_path = cell_file_path(project, cell) + os.makedirs(os.path.dirname(save_path), exist_ok=True) + + with open(save_path, 'w', encoding='utf-8') as f: + f.write(content) + + return jsonify({ + "message": "successfully saved", + "project": project, + "cell": cell, + "path": save_path + }), 200 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + + # --- API ROUTES (Library & Components) --- @app.route('/api/library') def getLib(): """Get library structure.""" - tree = buildTree(YML_PATH) + # tree = buildTree(YML_PATH) if os.path.isdir(COMPS_ROOT): compMap = findComps(COMPS_ROOT) fresh_tree = addCompsToTree(compMap) @@ -249,4 +423,8 @@ def getCompImg(component_name): if __name__ == '__main__': print("Starting mxpic EDA Server on http://127.0.0.1:3000") - app.run(host='127.0.0.1', port=3000, debug=True) \ No newline at end of file + app.run(host='127.0.0.1', port=3000, debug=True) + + + + diff --git a/backend/server_new.py b/backend/server_new.py deleted file mode 100644 index bf34a56..0000000 --- a/backend/server_new.py +++ /dev/null @@ -1,227 +0,0 @@ - -import os -import yaml -from collections import OrderedDict -from flask import Flask, jsonify, send_from_directory, request, redirect, url_for, session, render_template -from werkzeug.security import check_password_hash -import database -from flask import Response - -# --- Path Configurations --- -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend') - -YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml') -COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra') -# Define where your new icons folder is located (adjust if it's placed elsewhere) -ICONS_DIR = os.path.join(BASE_DIR, 'icons') - -#build layout save path -SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_layouts') - - -app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR) -app.secret_key = 'super_secret_mxpic_key' -app.json.sort_keys = False - -database.init_db() - -# ... [Keep countSpaces and buildTree exactly as they are] ... - -def findComps(baseDir): - """Scan component folders, return map of paths -> component info.""" - compMap = {} - refDir = baseDir - for root, dirs, files in os.walk(baseDir): - ymlFiles = [f for f in files if f.endswith('.yml')] - if ymlFiles: - parentDir = os.path.dirname(root) - relPath = os.path.relpath(parentDir, refDir) - parts = () if relPath == '.' else tuple(relPath.split(os.sep)) - compName = os.path.basename(root) - - # Extract the category (the mother folder's name) - category = os.path.basename(parentDir) - - compMap[parts] = { - 'folder': compName, - 'yml': ymlFiles[0], - 'category': category # Save the category to the map - } - dirs.clear() - return compMap - -def addCompsToTree(compMap): - """Build a completely fresh tree from scratch and insert component nodes.""" - fresh_tree = OrderedDict() - - for pathSeg, compItem in compMap.items(): - compName = compItem['folder'] - curNode = fresh_tree - - for seg in pathSeg: - if seg not in curNode: - curNode[seg] = OrderedDict() - curNode = curNode[seg] - - curNode[compName] = OrderedDict({ - "__type__": "component", - "__name__": compName, - "__yml__": compItem['yml'], - "__category__": compItem['category'] # Inject category into the tree - }) - - return fresh_tree - -# ... [Keep readCompYaml and Page Routes exactly as they are] ... - -# --- API ROUTES (Library, Components & Icons) --- - -@app.route('/api/icon/') -def getIcon(category): - """Serve the icon corresponding to the component category.""" - for ext in ('.png', '.svg', '.jpg'): - icon_path = os.path.join(ICONS_DIR, f"{category}{ext}") - if os.path.exists(icon_path): - return send_from_directory(ICONS_DIR, f"{category}{ext}") - - fallback = os.path.join(ICONS_DIR, "default.png") - if os.path.exists(fallback): - return send_from_directory(ICONS_DIR, "default.png") - - # return png if not found - transparent_png = ( - b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01' - b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01' - b'\x00\x00\x05\x00\x01\r\n\xf4\xc0\x00\x00\x00\x00IEND\xaeB`\x82' - ) - return Response(transparent_png, mimetype='image/png') - -# ... [Keep existing API routes below] ... - -def readCompYaml(compName): - """Load YAML from component folder.""" - for root, dirs, files in os.walk(COMPS_ROOT): - if os.path.basename(root) == compName: - dirs.clear() - ymlFiles = [f for f in files if f.endswith('.yml')] - if ymlFiles: - ymlPath = os.path.join(root, ymlFiles[0]) - with open(ymlPath, 'r', encoding='utf-8') as f: - return yaml.safe_load(f) - return None - -# --- AUTHENTICATION & PAGE ROUTES --- -@app.route('/') -def home(): - """Route to login page, or bypass to dashboard if already authenticated.""" - if 'user_id' in session: - return redirect(url_for('dashboard')) - return render_template('login.html') - -@app.route('/login', methods=['POST']) -def login(): - """Verify credentials against the database.""" - username = request.form.get('username') - password = request.form.get('password') - - user = database.get_user(username) - - # Verify hash from database matches entered password - if user and check_password_hash(user[2], password): - session['user_id'] = user[0] - session['username'] = user[1] - return redirect(url_for('dashboard')) - else: - return render_template('login.html', error="Invalid username or password") - -@app.route('/dashboard') -def dashboard(): - """User project list.""" - if 'user_id' not in session: - return redirect(url_for('home')) - - return render_template('dashboard.html', username=session['username']) - -@app.route('/canvas') -def canvas(): - """The main EDA editor.""" - if 'user_id' not in session: - return redirect(url_for('home')) - - # Note: Ensure your old index.html is renamed to canvas.html in the frontend folder - return render_template('canvas.html') - -@app.route('/logout') -def logout(): - """Clear session and return to login.""" - session.clear() - return redirect(url_for('home')) - - - - -@app.route('/api/save-layout', methods=['POST']) -def save_layout(): - try: - data = request.get_json() - filename = data.get('filename', 'layout.yaml') - content = data.get('content', '') - - os.makedirs(SAVE_DIR, exist_ok=True) - - save_path = os.path.join(SAVE_DIR, filename) - - with open(save_path, 'w', encoding='utf-8') as f: - f.write(content) - - return jsonify({ - "message": "successfully saved", - "path": save_path - }), 200 - - except Exception as e: - return jsonify({"error": str(e)}), 500 - - - -# --- API ROUTES (Library & Components) --- -@app.route('/api/library') -def getLib(): - """Get library structure.""" - # tree = buildTree(YML_PATH) - if os.path.isdir(COMPS_ROOT): - compMap = findComps(COMPS_ROOT) - fresh_tree = addCompsToTree(compMap) - return jsonify(fresh_tree) - - - -@app.route('/api/component/') -def getComp(component_name): - """Return component YAML data.""" - data = readCompYaml(component_name) - if data is None: - return jsonify({"error": "Component not found"}), 404 - return jsonify(data) - -@app.route('/api/component//image') -def getCompImg(component_name): - """Return first image in component folder.""" - for root, dirs, files in os.walk(COMPS_ROOT): - if os.path.basename(root) == component_name: - dirs.clear() - for ext in ('.png', '.jpg', '.jpeg', '.svg'): - for f in files: - if f.lower().endswith(ext): - return send_from_directory(root, f) - break - return jsonify({"error": "No image found"}), 404 - -if __name__ == '__main__': - print("Starting mxpic EDA Server on http://127.0.0.1:3000") - app.run(host='127.0.0.1', port=3000, debug=True) - - - - \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/.project.json b/database/admin/layout/mxpic_project_1/.project.json new file mode 100644 index 0000000..4f868b9 --- /dev/null +++ b/database/admin/layout/mxpic_project_1/.project.json @@ -0,0 +1,4 @@ +{ + "name": "mxpic_project_1", + "technology": "Silterra/EMO1_2ML_CU_Al_RDL" +} \ No newline at end of file diff --git a/backend/generated_layouts/comp_1.yaml b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml similarity index 59% rename from backend/generated_layouts/comp_1.yaml rename to database/admin/layout/mxpic_project_1/mxpic_project_1.yml index a16bc9c..53497d7 100644 --- a/backend/generated_layouts/comp_1.yaml +++ b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml @@ -1,8 +1,11 @@ # ============================================= # mxPIC Cell/Project Definition File # ============================================= -name: comp_1 -type: composite +schema_version: "2.0.0" +kind: cell +project: mxpic_project_1 +name: mxpic_project_1 +type: project version: "1.0.0" # 1. External Ports (How this cell connects to the outside world) @@ -28,32 +31,23 @@ ports: # 2. Instances (The sub-components dropped onto this canvas) instances: - component_1: - component: EMO1_2ML_CU_Al_RDL/composite/Mach_Zender_modulators/MZM_800G_L3000_GSSG_TRAIL_TypeX5_QY_v1_20260303 - x: 100.0 - y: 100.0 + canvas_1: + component: canvas_1 + x: 250.0 + y: 200.0 rotation: 0.0 mirror: false settings: - length: + length: component_2: - component: EMO1_2ML_CU_Al_RDL/electronics/inductors/INDC_200pH_SiNPP_QY_202604 - x: 400.0 - y: 100.0 + component: canvas_1 + x: 250.0 + y: 280.0 rotation: 0.0 mirror: false settings: - length: - - component_3: - component: EMO1_2ML_CU_Al_RDL/electronics/pads/Spec_PADs_ABCD_292_P125_250_W80_80_QY_20260324 - x: 700.0 - y: 100.0 - rotation: 0.0 - mirror: false - settings: - length: + length: # 3. Bundles (Grouped links for multi-bus/parallel routing) bundles: diff --git a/database/admin/layout/mxpic_project_1_2/.project.json b/database/admin/layout/mxpic_project_1_2/.project.json new file mode 100644 index 0000000..4a24707 --- /dev/null +++ b/database/admin/layout/mxpic_project_1_2/.project.json @@ -0,0 +1,4 @@ +{ + "name": "mxpic_project_1_2", + "technology": "Silterra/EMO1_2ML_CU_Al_RDL" +} \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1_3/.project.json b/database/admin/layout/mxpic_project_1_3/.project.json new file mode 100644 index 0000000..ccec53e --- /dev/null +++ b/database/admin/layout/mxpic_project_1_3/.project.json @@ -0,0 +1,4 @@ +{ + "name": "mxpic_project_1_3", + "technology": "Silterra/EMO1_2ML_CU_Al_RDL" +} \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1_4/.project.json b/database/admin/layout/mxpic_project_1_4/.project.json new file mode 100644 index 0000000..929a8c6 --- /dev/null +++ b/database/admin/layout/mxpic_project_1_4/.project.json @@ -0,0 +1,4 @@ +{ + "name": "mxpic_project_1_4", + "technology": "Silterra/EMO1_2ML_CU_Al_RDL" +} \ No newline at end of file diff --git a/frontend/canvas.html b/frontend/canvas.html index e8be100..b21e426 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -8,7 +8,7 @@ mxPIC Core - Canvas - + @@ -17,14 +17,44 @@ @@ -500,7 +780,7 @@ transition: 'none', boxSizing: 'border-box', boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)', - fontFamily: "'Inter', sans-serif", + fontFamily: "'IBM Plex Sans', sans-serif", }}>
{!data.hideIcon && data.category && ( @@ -511,7 +791,8 @@
{data.componentDisplayName}
+ {data.componentName && data.componentName !== data.componentDisplayName && ( +
+ {data.componentName} +
+ )}
@@ -536,6 +830,7 @@ prevProps.id === nextProps.id && prevProps.selected === nextProps.selected && prevProps.data.componentDisplayName === nextProps.data.componentDisplayName && + prevProps.data.componentName === nextProps.data.componentName && prevProps.data.category === nextProps.data.category && prevProps.data.rotation === nextProps.data.rotation && prevProps.data.hideIcon === nextProps.data.hideIcon @@ -564,11 +859,131 @@ ); }; + const EditableCanvasTabName = ({ page, active, onRename }) => { + const [value, setValue] = useState(page.name); + + useEffect(() => { + setValue(page.name); + }, [page.name, page.id]); + + if (!active || page.type === 'project') { + return {page.name}; + } + + const commit = () => { + const nextName = value.trim(); + if (nextName && nextName !== page.name) { + onRename(page.id, nextName); + } else { + setValue(page.name); + } + }; + + return ( + setValue(event.target.value)} + onBlur={commit} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === 'Enter') event.currentTarget.blur(); + if (event.key === 'Escape') { + setValue(page.name); + event.currentTarget.blur(); + } + }} + /> + ); + }; + + const EditableTreeCanvasName = ({ pageId, name, canRename, onRename, onOpen }) => { + const [value, setValue] = useState(name); + + useEffect(() => { + setValue(name); + }, [name]); + + if (!canRename) { + return ( + + {name} + + ); + } + + const commit = () => { + const nextName = value.trim(); + if (nextName && nextName !== name) { + onRename(pageId, nextName); + } else { + setValue(name); + } + }; + + return ( + event.stopPropagation()} + onChange={(event) => setValue(event.target.value)} + onBlur={commit} + onKeyDown={(event) => { + if (event.key === 'Enter') event.currentTarget.blur(); + if (event.key === 'Escape') { + setValue(name); + event.currentTarget.blur(); + } + }} + /> + ); + }; + + const isLibraryComponentLeaf = (node) => node && node.__type__ === 'component'; + + const getCategoryComponents = (categoryNode) => { + return Object.entries(categoryNode || {}) + .filter(([, childData]) => isLibraryComponentLeaf(childData)) + .map(([childName, childData]) => ({ + name: childData.__name__ || childName, + category: childData.__category__ || childData.__name__ || childName + })); + }; + + const CategoryCard = ({ name, components = [] }) => { + const componentNames = components.map(component => component.name).filter(Boolean); + const firstComponent = componentNames[0] || name; + + const handleDragStart = (event) => { + const dragData = JSON.stringify({ + type: 'category', + category: name, + name: firstComponent, + components: componentNames + }); + event.dataTransfer.setData('application/reactflow', dragData); + event.dataTransfer.setData('text/plain', dragData); + event.dataTransfer.effectAllowed = 'move'; + }; + + return ( +
+
+ +
+
+ {name} +
+
{componentNames.length} components
+
+ ); + }; const TreeNode = ({ name, children }) => { if (children && children.__type__ === 'component') { const componentName = children.__name__; const componentCategory = children.__category__ || 'default'; + const isUserCell = children.__cell__ === true; const dragStartPos = useRef(null); const dragReady = useRef(false); @@ -587,13 +1002,14 @@ }; const handleDragStart = (event) => { - if (!dragReady.current) { - event.preventDefault(); - return false; - } - const dragData = JSON.stringify({ name: componentName, category: componentCategory }); - console.log("๐Ÿš€ DRAG START: Sending data ->", dragData); + const dragData = JSON.stringify( + isUserCell + ? { name: componentName, type: 'composite' } + : { name: componentName, category: componentCategory } + ); + console.log("DRAG START: Sending data ->", dragData); event.dataTransfer.setData('application/reactflow', dragData); + event.dataTransfer.setData('text/plain', dragData); event.dataTransfer.effectAllowed = 'move'; dragStartPos.current = null; dragReady.current = false; @@ -623,22 +1039,44 @@ ); } - const hasChildren = children && Object.keys(children).length > 0; + const entries = children ? Object.entries(children) : []; + const hasChildren = entries.length > 0; + const isComponentGrid = hasChildren && entries.every(([, childData]) => isLibraryComponentLeaf(childData)); + const isCategoryGrid = hasChildren && entries.every(([, childData]) => { + const childEntries = Object.entries(childData || {}); + return childEntries.length > 0 && childEntries.every(([, grandChild]) => isLibraryComponentLeaf(grandChild)); + }); return (
- ๐Ÿ“‚ {name} + + + {name} + > + - {hasChildren && - Object.entries(children).map(([childName, childData]) => ( - - )) - } + {hasChildren && ( + isCategoryGrid ? ( +
+ {entries.map(([childName, childData]) => ( + + ))} +
+ ) : isComponentGrid ? ( +
+ +
+ ) : ( + entries.map(([childName, childData]) => ( + + )) + ) + )}
); }; - const ProjectTreeNode = ({ name, children, onOpenComposite, onOpenProject }) => { + const ProjectTreeNode = ({ name, children, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas }) => { if (children && children.__type__ === 'project') { const projectName = children.__name__ || name; const composites = children.composites || []; @@ -648,34 +1086,65 @@ return (
- ๐Ÿ“ {name} + + + Project - {name} + > + {composites.map(comp => ( - + ))}
); } + if (children && children.__type__ === 'instance') { + const instanceName = children.__instance__ || name; + const pageName = children.__page__; + return ( +
onSelectInstance && onSelectInstance(pageName, instanceName)} + > + [] + {instanceName} +
+ ); + } + if (children && children.__type__ === 'composite') { const compositeName = children.__name__ || name; + const cellName = children.__cellName__ || compositeName; const tree = children.tree || {}; const handleDragStart = (event) => { - const dragData = JSON.stringify({ name: compositeName, type: 'composite' }); + const dragData = JSON.stringify({ name: cellName, type: 'composite' }); event.dataTransfer.setData('application/reactflow', dragData); event.dataTransfer.effectAllowed = 'move'; }; - const handleDoubleClick = () => { - if (onOpenComposite) onOpenComposite(compositeName); + const handleOpen = (event) => { + if (event.target.closest('.tree-expander')) return; + if (onOpenComposite) onOpenComposite(cellName); }; return (
- - โ– {name} + + + + onOpenComposite && onOpenComposite(cellName)} + /> + > + {Object.keys(tree).length > 0 ? ( Object.entries(tree).map(([childName, childData]) => ( - + )) ) : (
No components
@@ -710,25 +1179,30 @@ return (
- ๐Ÿ“ {name} + + + {name} + > + {Object.entries(children).map(([childName, childData]) => ( - + ))}
); }; - const CompositeComponentTree = ({ name, children }) => { + const CompositeComponentTree = ({ name, children, canvasName, onSelectInstance }) => { if (children && children.__type__ === 'component') { - const instances = children.instances || []; - const displayText = instances.length > 0 - ? instances.join(', ') - : (children.__name__ || name); + const displayText = children.__instance__ || name; return ( -
- โ– +
onSelectInstance && onSelectInstance(canvasName, displayText)} + > + [] {displayText}
); @@ -738,11 +1212,15 @@ return (
- ๐Ÿ“‚ {name} + + + {name} + > + {hasChildren && Object.entries(children).map(([childName, childData]) => ( - + )) }
@@ -751,80 +1229,48 @@ return null; }; - const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, activePage, onPortChange, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => { - const [portX, setPortX] = useState(''); - const [portY, setPortY] = useState(''); - const [portA, setPortA] = useState(''); - const [activeBlock, setActiveBlock] = useState('project'); + const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => { + const [projectPanelHeight, setProjectPanelHeight] = useState(270); + const [resizingProjectPanel, setResizingProjectPanel] = useState(false); + const leftPanelRef = useRef(null); useEffect(() => { - if (activePage) { - setPortX(activePage.port.x.toString()); - setPortY(activePage.port.y.toString()); - setPortA(activePage.port.a.toString()); - } else { - setPortX(''); - setPortY(''); - setPortA(''); - } - }, [activePage?.id, activePage?.port.x, activePage?.port.y, activePage?.port.a]); - - const handleSubmitX = () => { - if (!activePage) return; - const val = parseFloat(portX); - if (!isNaN(val)) { - onPortChange(activePage.id, { ...activePage.port, x: val }); - } else { - setPortX(activePage.port.x.toString()); - } - }; - - const handleSubmitY = () => { - if (!activePage) return; - const val = parseFloat(portY); - if (!isNaN(val)) { - onPortChange(activePage.id, { ...activePage.port, y: val }); - } else { - setPortY(activePage.port.y.toString()); - } - }; - - const handleSubmitA = () => { - if (!activePage) return; - const val = parseFloat(portA); - if (!isNaN(val)) { - onPortChange(activePage.id, { ...activePage.port, a: val }); - } else { - setPortA(activePage.port.a.toString()); - } - }; + if (!resizingProjectPanel) return; + const onMouseMove = (event) => { + if (!leftPanelRef.current) return; + const rect = leftPanelRef.current.getBoundingClientRect(); + const nextHeight = event.clientY - rect.top - 12; + setProjectPanelHeight(Math.min(620, Math.max(150, nextHeight))); + }; + const onMouseUp = () => setResizingProjectPanel(false); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, [resizingProjectPanel]); const handleProjectToggle = () => { - if (!projectExpanded) { - setActiveBlock('project'); - } onProjectToggle(); }; const handleLibraryToggle = () => { - if (!expanded) { - setActiveBlock('library'); - } onToggle(); }; return ( ); }; @@ -970,11 +1378,17 @@ }, [onUpdateNode]); const formatPort = (port) => { - if (!port) return 'โ€”'; + if (!port) return '-'; return `x:${port.x ?? '?'}, y:${port.y ?? '?'}, a:${port.a ?? '?'}, w:${port.width ?? '?'}`; }; const currentComponentDisplayName = selectedNode?.data?.componentDisplayName || ''; + const selectedComponentName = selectedNode?.data?.componentName || ''; + const availableComponentsFromNode = Array.isArray(selectedNode?.data?.availableComponents) + ? selectedNode.data.availableComponents.filter(Boolean) + : []; + const availableComponents = Array.from(new Set([...availableComponentsFromNode, selectedComponentName].filter(Boolean))); + const canChooseComponent = availableComponentsFromNode.length > 0; const handleStartEditName = () => { setTempComponentName(currentComponentDisplayName); @@ -1081,6 +1495,27 @@
Parameters
+ {canChooseComponent && ( +
+ + +
+ )} {loading ? (

Loading data...

) : componentData ? ( @@ -1114,7 +1549,7 @@ title="Click to edit" > {currentComponentDisplayName || componentData.name} - โœŽ + Edit
)}
@@ -1229,26 +1664,11 @@ const compName = node.data.componentName; const instanceName = node.data.componentDisplayName || node.id; if (!compName) return; - const fullPath = findComponentPath(library, compName); - if (fullPath.length === 0) return; - - let current = tree; - for (let i = 0; i < fullPath.length - 1; i++) { - const seg = fullPath[i]; - if (!current[seg]) current[seg] = {}; - current = current[seg]; - } - const lastDir = fullPath[fullPath.length - 1]; - if (!current[lastDir]) { - current[lastDir] = { - __type__: 'component', - __name__: compName, - instances: [] - }; - } - if (!current[lastDir].instances.includes(instanceName)) { - current[lastDir].instances.push(instanceName); - } + tree[instanceName] = { + __type__: 'component', + __name__: compName, + __instance__: instanceName + }; }); return tree; } @@ -1276,6 +1696,10 @@ } function App() { + const currentProjectName = useMemo(() => { + const params = new URLSearchParams(window.location.search); + return params.get('project') || 'project_1'; + }, []); const [pages, setPages] = useState([]); const [activePageId, setActivePageId] = useState(null); const reactFlowInstance = useReactFlow(); @@ -1294,6 +1718,8 @@ const [dragging, setDragging] = useState(null); const [gridSnap, setGridSnap] = useState(false); + const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark'); + const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]); const [clipboard, setClipboard] = useState({ nodes: [] }); @@ -1307,6 +1733,15 @@ const [standaloneComposites, setStandaloneComposites] = useState([]); const [compositeTrees, setCompositeTrees] = useState({}); + useEffect(() => { + document.body.classList.toggle('light-mode', themeMode === 'light'); + localStorage.setItem('mxpic-theme', themeMode); + }, [themeMode]); + + const addLog = useCallback((message) => { + setLogs(prev => [...prev.slice(-80), { time: new Date().toLocaleTimeString(), message }]); + }, []); + const syncCompositePlacement = useCallback((projectName, compositeName, mode = 'add') => { setStandaloneComposites(prev => { if (mode === 'add') return prev.filter(name => name !== compositeName); @@ -1689,108 +2124,135 @@ }, [pages]); useEffect(() => { - if (library && !initializedRef.current) { - initializedRef.current = true; - const compList = collectComponentNames(library); + if (!library || initializedRef.current) return; + initializedRef.current = true; - const projectId = Date.now().toString() + Math.random().toString(36).substr(2, 5); - const projectPage = { - id: projectId, - name: 'MainProject', - type: 'project', - nodes: [], - edges: [], - port: { x: 0, y: 0, a: 0 } + const makeProjectPage = () => ({ + id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + name: currentProjectName, + type: 'project', + nodes: [], + edges: [], + port: { x: 0, y: 0, a: 0 } + }); + + const findCategory = (compName) => { + let category = ''; + const walk = (obj) => { + if (obj?.__type__ === 'component' && obj.__name__ === compName) { + category = obj.__category__ || ''; + return true; + } + if (typeof obj === 'object') { + for (const v of Object.values(obj)) if (walk(v)) return true; + } + return false; }; + walk(library); + return category; + }; - let counter = 1; - const fixedComps1 = compList.slice(0, 3); - const compNodes1 = fixedComps1.map((comp, i) => { - const name = `component_${counter++}`; - return { - id: `node-${Date.now()}-${i}-${Math.random().toString(36).substr(2, 5)}`, + const pageFromYaml = (cellName, content) => { + const doc = jsyaml.load(content) || {}; + const nodeNameMap = {}; + const nodes = [ + { + id: 'page-port', + type: 'portNode', + position: { x: 50, y: 150 }, + data: { label: 'Port', angle: 0 }, + draggable: true, + selectable: true, + deletable: false, + } + ]; + const edges = []; + + Object.entries(doc.instances || {}).forEach(([instName, inst]) => { + const compPath = inst.component || ''; + const compName = compPath.split('/').pop(); + const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; + nodeNameMap[instName] = nodeId; + nodes.push({ + id: nodeId, type: 'rotatableNode', - position: { x: 100 + i * 300, y: 100 }, + position: { + x: parseFloat(inst.x) || 0, + y: parseFloat(inst.y) || 0, + }, data: { - label: comp.name, - componentName: comp.name, - category: comp.category, - rotation: 0, - componentDisplayName: name - } - }; + label: compName, + componentName: compName, + category: findCategory(compName), + rotation: parseFloat(inst.rotation) || 0, + componentDisplayName: instName, + }, + }); }); - const comp1Id = Date.now().toString() + Math.random().toString(36).substr(2, 5); - const comp1Page = { - id: comp1Id, - name: 'comp_1', - type: 'composite', - nodes: [ - { - id: 'page-port', - type: 'portNode', - position: { x: 50, y: 150 }, - data: { label: 'Port', angle: 0 }, - draggable: true, - selectable: true, - deletable: false, - }, - ...compNodes1 - ], - edges: [], + const links = doc.bundles?.output_bus?.links; + if (links) { + const linkArray = Array.isArray(links) ? links : [links]; + linkArray.forEach(link => { + if (!link.from || !link.to) return; + const [fromInst, fromPort] = link.from.split(':'); + const [toInst, toPort] = link.to.split(':'); + const sourceId = nodeNameMap[fromInst]; + const targetId = nodeNameMap[toInst]; + if (!sourceId || !targetId) return; + edges.push({ + id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`, + source: sourceId, + target: targetId, + sourceHandle: fromPort, + targetHandle: toPort, + type: 'smoothstep', + style: { stroke: 'var(--accent)', strokeWidth: 2 }, + }); + }); + } + + return { + id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`, + name: doc.name || cellName, + type: doc.type === 'project' ? 'project' : 'composite', + nodes, + edges, port: { x: 50, y: 150, a: 0 } }; + }; - const fixedComps2 = compList.slice(0, 3); - const compNodes2 = fixedComps2.map((comp, i) => { - const name = `component_${counter++}`; - return { - id: `node-${Date.now()}-${i}-${Math.random().toString(36).substr(2, 10)}`, - type: 'rotatableNode', - position: { x: 100 + i * 300, y: 200 }, - data: { - label: comp.name, - componentName: comp.name, - category: comp.category, - rotation: 0, - componentDisplayName: name - } - }; - }); + const loadProject = async () => { + const projectPage = makeProjectPage(); + try { + const response = await fetch(`/api/projects/${encodeURIComponent(currentProjectName)}`); + if (!response.ok) { + setPages([projectPage]); + setActivePageId(projectPage.id); + setProjectCompositeMap({ [currentProjectName]: [] }); + return; + } - componentCounterRef.current = counter; + const data = await response.json(); + const cellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content)); + setPages([projectPage, ...cellPages]); + setActivePageId(projectPage.id); + setProjectCompositeMap({ [currentProjectName]: cellPages.map(page => page.name) }); + setStandaloneComposites([]); + const nextTrees = {}; + cellPages.forEach(page => { + nextTrees[page.name] = buildCompInstanceTree(page.nodes.filter(n => n.id !== 'page-port' && n.data?.componentName), library); + }); + setCompositeTrees(nextTrees); + } catch (error) { + setPages([projectPage]); + setActivePageId(projectPage.id); + setProjectCompositeMap({ [currentProjectName]: [] }); + } + }; - const comp2Id = Date.now().toString() + Math.random().toString(36).substr(2, 5); - const comp2Page = { - id: comp2Id, - name: 'comp_2', - type: 'composite', - nodes: [ - { - id: 'page-port', - type: 'portNode', - position: { x: 50, y: 250 }, - data: { label: 'Port', angle: 0 }, - draggable: true, - selectable: true, - deletable: false, - }, - ...compNodes2 - ], - edges: [], - port: { x: 50, y: 250, a: 0 } - }; - - setPages([projectPage, comp1Page, comp2Page]); - setActivePageId(projectId); - setProjectCompositeMap({ MainProject: ['comp_1', 'comp_2'] }); - setCompositeTrees({ - comp_1: buildCompInstanceTree(comp1Page.nodes.filter(n => n.id !== 'page-port' && n.data?.componentName), library), - comp_2: buildCompInstanceTree(comp2Page.nodes.filter(n => n.id !== 'page-port' && n.data?.componentName), library) - }); - } - }, [library, collectComponentNames]); + loadProject(); + }, [library, currentProjectName]); useEffect(() => { if (activePage && reactFlowInstance) { @@ -1805,6 +2267,28 @@ const selectedNode = useMemo(() => currentNodes.find(n => n.selected), [currentNodes]); + const selectInstanceInPage = useCallback((pageName, instanceName) => { + if (!pageName || !instanceName) return; + const targetPage = pages.find(p => p.name === pageName); + if (!targetPage) return; + setActivePageId(targetPage.id); + setPages(prev => prev.map(page => { + if (page.id !== targetPage.id) { + return { + ...page, + nodes: page.nodes.map(node => ({ ...node, selected: false })) + }; + } + return { + ...page, + nodes: page.nodes.map(node => ({ + ...node, + selected: node.id !== 'page-port' && (node.data?.componentDisplayName === instanceName || node.id === instanceName) + })) + }; + })); + }, [pages]); + const openProject = useCallback((name) => { setPages(prev => { const existing = prev.find(p => p.name === name && p.type === 'project'); @@ -1860,6 +2344,98 @@ }); }, [projectCompositeMap, standaloneComposites]); + const renameCanvas = useCallback((pageId, requestedName) => { + const normalizedName = requestedName.trim().replace(/[^A-Za-z0-9_.-]+/g, '_').replace(/^[._]+|[._]+$/g, ''); + if (!normalizedName) return; + const pageToRename = pages.find(p => p.id === pageId); + if (!pageToRename || pageToRename.type === 'project' || pageToRename.name === normalizedName) return; + const nameTaken = pages.some(p => p.id !== pageId && p.name === normalizedName); + if (nameTaken) { + addLog(`Canvas rename failed: "${normalizedName}" already exists.`); + return; + } + const oldName = pageToRename.name; + setPages(prev => prev.map(p => { + const renamedPage = p.id === pageId ? { ...p, name: normalizedName } : p; + return { + ...renamedPage, + nodes: renamedPage.nodes.map(node => { + if (node.data?.type === 'composite' && node.data.componentName === oldName) { + return { + ...node, + data: { + ...node.data, + componentName: normalizedName, + componentDisplayName: node.data.componentDisplayName === oldName ? normalizedName : node.data.componentDisplayName, + label: normalizedName + } + }; + } + return node; + }) + }; + })); + setProjectCompositeMap(prev => { + const next = {}; + Object.entries(prev).forEach(([project, cells]) => { + next[project] = cells.map(name => name === oldName ? normalizedName : name); + }); + return next; + }); + setStandaloneComposites(prev => prev.map(name => name === oldName ? normalizedName : name)); + setCompositeTrees(prev => { + const next = { ...prev }; + if (Object.prototype.hasOwnProperty.call(next, oldName)) { + next[normalizedName] = next[oldName]; + delete next[oldName]; + } + return next; + }); + fetch(`/api/projects/${encodeURIComponent(currentProjectName)}/cells/${encodeURIComponent(oldName)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: normalizedName }) + }).then(response => { + if (response.ok) addLog(`Renamed canvas "${oldName}" to "${normalizedName}".`); + }).catch(() => addLog(`Renamed canvas locally; saved file rename did not complete.`)); + }, [pages, currentProjectName, addLog]); + + const createCell = useCallback(() => { + const existingNames = new Set(pages.filter(p => p.type === 'composite').map(p => p.name)); + let index = existingNames.size + 1; + let cellName = `canvas_${index}`; + while (existingNames.has(cellName)) { + index += 1; + cellName = `canvas_${index}`; + } + + const newCell = { + id: Date.now().toString() + Math.random().toString(36).substr(2, 5), + name: cellName, + type: 'composite', + nodes: [ + { + id: 'page-port', + type: 'portNode', + position: { x: 50, y: 150 }, + data: { label: 'Port', angle: 0 }, + draggable: true, + selectable: true, + deletable: false, + } + ], + edges: [], + port: { x: 50, y: 150, a: 0 } + }; + + setPages(prev => [...prev, newCell]); + setActivePageId(newCell.id); + setProjectCompositeMap(prev => ({ + ...prev, + [currentProjectName]: [...(prev[currentProjectName] || []), cellName] + })); + }, [pages, currentProjectName]); + const closePage = useCallback((pageId) => { setPages(prev => { const pageToClose = prev.find(p => p.id === pageId); @@ -1910,7 +2486,7 @@ const onDrop = useCallback((event) => { event.preventDefault(); const rawData = event.dataTransfer.getData('application/reactflow'); - console.log("๐Ÿ“ฅ DROP EVENT: Received raw data ->", rawData); + console.log("?๎™ฅ DROP EVENT: Received raw data ->", rawData); if (!rawData) return; let parsedData; try { @@ -1940,15 +2516,24 @@ if (activePage?.type === 'project') { const projectName = activePage.name; setStandaloneComposites(prev => prev.filter(name => name !== parsedData.name)); - setProjectCompositeMap(prev => ({ - ...prev, - [projectName]: [...(prev[projectName] || []), parsedData.name] - })); + setProjectCompositeMap(prev => { + const currentList = prev[projectName] || []; + const instanceName = newNode.data.componentDisplayName || parsedData.name; + if (currentList.includes(instanceName)) return prev; + return { + ...prev, + [projectName]: [...currentList, instanceName] + }; + }); } return; } if (parsedData.type === 'composite') { if (!activePageId) return; + if (activePage?.type === 'composite' && parsedData.name === activePage.name) { + addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`); + return; + } const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }); const newNode = { id: Date.now().toString(), @@ -1968,7 +2553,15 @@ return { ...p, nodes: p.nodes.concat(newNode) }; })); if (activePage?.type === 'project') { - syncCompositePlacement(activePage.name, parsedData.name, 'add'); + setProjectCompositeMap(prev => { + const currentList = prev[activePage.name] || []; + const instanceName = newNode.data.componentDisplayName || parsedData.name; + if (currentList.includes(instanceName)) return prev; + return { + ...prev, + [activePage.name]: [...currentList, instanceName] + }; + }); } return; } @@ -1977,6 +2570,38 @@ return; } const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }); + if (parsedData.type === 'category') { + const availableComponents = Array.isArray(parsedData.components) + ? parsedData.components + .map(component => typeof component === 'string' ? component : component?.name) + .filter(Boolean) + : []; + const selectedComponent = parsedData.name || availableComponents[0] || parsedData.category; + if (!selectedComponent) { + addLog('Skipped category placement: no components were found in this library category.'); + return; + } + const componentDisplayName = generateComponentDisplayName(); + const newNode = { + id: Date.now().toString(), + type: 'rotatableNode', + position, + data: { + label: selectedComponent, + componentName: selectedComponent, + availableComponents: availableComponents.length > 0 ? availableComponents : [selectedComponent], + libraryCategory: parsedData.category || 'default', + category: parsedData.category || 'default', + rotation: 0, + componentDisplayName: componentDisplayName + }, + }; + setPages(prev => prev.map(p => { + if (p.id !== activePageId) return p; + return { ...p, nodes: p.nodes.concat(newNode) }; + })); + return; + } const componentDisplayName = generateComponentDisplayName(); const newNode = { id: Date.now().toString(), @@ -2057,19 +2682,45 @@ const items = []; const projectPages = pages.filter(p => p.type === 'project'); projectPages.forEach(project => { - const composites = (projectCompositeMap[project.name] || []).map(name => { - const compPage = pages.find(p => p.name === name && p.type === 'composite'); - return { - __type__: 'composite', - __name__: name, - tree: compositeTrees[name] || {}, - pageId: compPage ? compPage.id : name - }; - }); + const projectNodeItems = project.nodes + .filter(node => node.id !== 'page-port' && node.data?.componentName) + .map(node => { + const instanceName = node.data.componentDisplayName || node.id; + const componentName = node.data.componentName; + const compPage = pages.find(p => p.name === componentName && p.type === 'composite'); + if (compPage) { + return { + __type__: 'composite', + __name__: instanceName, + __cellName__: componentName, + tree: compositeTrees[componentName] || {}, + pageId: compPage.id + }; + } + return { + __type__: 'instance', + __name__: instanceName, + __instance__: instanceName, + __page__: project.name + }; + }); + + const unplacedCells = (projectCompositeMap[project.name] || []) + .filter(name => !projectNodeItems.some(item => item.__name__ === name || item.__cellName__ === name)) + .map(name => { + const compPage = pages.find(p => p.name === name && p.type === 'composite'); + return { + __type__: 'composite', + __name__: name, + __cellName__: name, + tree: compositeTrees[name] || {}, + pageId: compPage ? compPage.id : name + }; + }); items.push({ type: 'project', name: project.name, - composites: composites + composites: [...projectNodeItems, ...unplacedCells] }); }); standaloneComposites.forEach(name => { @@ -2083,6 +2734,24 @@ return items; }, [pages, library, projectCompositeMap, standaloneComposites, compositeTrees]); + const libraryWithCells = useMemo(() => { + const cellEntries = {}; + pages + .filter(page => page.type === 'composite') + .forEach(page => { + cellEntries[page.name] = { + __type__: 'component', + __name__: page.name, + __category__: 'composite', + __cell__: true + }; + }); + return { + ...cellEntries, + ...(library || {}) + }; + }, [pages, library]); + const buildBundlesYaml = (page) => { const { nodes, edges } = page; const nodeMap = {}; @@ -2115,8 +2784,11 @@ ${linksYaml}`; const header = `# ============================================= # mxPIC Cell/Project Definition File # ============================================= +schema_version: "2.0.0" +kind: cell +project: ${currentProjectName} name: ${activePage.name} -type: ${'composite'} +type: ${activePage.type === 'project' ? 'project' : 'composite'} version: "1.0.0" # 1. External Ports (How this cell connects to the outside world) @@ -2148,8 +2820,9 @@ instances:`; const compositeNodes = activePage.nodes.filter(n => n.type === 'rotatableNode' && n.data?.type === 'composite'); instancesBlock = compositeNodes.map(n => { const instanceName = n.data.componentDisplayName || n.data.componentName; + const compName = n.data.componentName || ''; return ` ${instanceName}: - component: + component: ${compName} x: ${n.position.x.toFixed(1)} y: ${n.position.y.toFixed(1)} rotation: ${(n.data.rotation || 0).toFixed(1)} @@ -2196,23 +2869,24 @@ ${bundlesBlock}`; method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - filename: `${activePage.name}.yaml`, // file name + project: currentProjectName, + cell: activePage.name, content: yamlContent, }), }); if (!response.ok) { const errData = await response.json(); - alert(errData.error || 'Save failed, unknown error'); + addLog(errData.error || 'Save failed, unknown error'); return; } const result = await response.json(); - alert('successfully saved : ' + result.path); + addLog('Successfully saved: ' + result.path); } catch (err) { - alert('save error: ' + err.message); + addLog('Save error: ' + err.message); } - }, [activePage, library, buildBundlesYaml, findComponentPath]); + }, [activePage, library, buildBundlesYaml, findComponentPath, currentProjectName, addLog]); const onNodeDoubleClick = useCallback((event, node) => { if (node.data?.type === 'composite') { @@ -2224,12 +2898,12 @@ ${bundlesBlock}`;
document.getElementById('open-yaml-input').click()}> - ๐Ÿ“‚ Open Project + Open Project +
+
+ + Cell
{pages.map(page => (
switchPage(page.id)}> - {page.name} - + +
))}
@@ -2253,8 +2930,8 @@ ${bundlesBlock}`;
Snap to Grid
+ + +
{activePage && ( @@ -2281,15 +2967,15 @@ ${bundlesBlock}`; bottom: 20, right: 20, zIndex: 10, - background: 'var(--accent)', - color: 'var(--bg-main)', + background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))', + color: '#04101f', border: 'none', padding: '12px 20px', - borderRadius: 6, - fontWeight: '600', + borderRadius: 8, + fontWeight: '700', cursor: 'pointer', fontSize: '0.85em', - boxShadow: '0 2px 8px rgba(0,0,0,0.3)', + boxShadow: '0 16px 34px rgba(37,99,235,0.24)', }} > Build Layout @@ -2317,6 +3003,11 @@ ${bundlesBlock}`;
+
+ {logs.map((entry, index) => ( +
[{entry.time}] {entry.message}
+ ))} +
@@ -2343,4 +3034,4 @@ ${bundlesBlock}`; -{% endraw %} \ No newline at end of file +{% endraw %} diff --git a/frontend/canvas_edit.html b/frontend/canvas_edit.html deleted file mode 100644 index 039070d..0000000 --- a/frontend/canvas_edit.html +++ /dev/null @@ -1,829 +0,0 @@ - - - -{% raw %} - - - - mxPIC Core - Canvas - - - - - - - - - - -
- - - - - -{% endraw %} \ No newline at end of file diff --git a/frontend/canvas_legacy.html b/frontend/canvas_legacy.html deleted file mode 100644 index 2d33d63..0000000 --- a/frontend/canvas_legacy.html +++ /dev/null @@ -1,750 +0,0 @@ - - - -{% raw %} - - - - Canvas - - - - - - - - - -
- - - - - -{% endraw %} diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 37ad02d..39ff643 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -5,25 +5,56 @@ Dashboard - - + - -
optihk
+ Logout
-

Welcome back, {{ username }}!

-
Your Recent Layouts
-
    -
  • - ๐Ÿ“„ - 400G_Transceiver_v1.gds -
  • -
  • - ๐Ÿ“„ - Ring_Modulator_Test.gds -
  • +
    Your Projects
    +
      +
    • Loading projects...
    - -
    +
    - -
    - -
    - -
    Powered by mxpic core
    +
    +
    [system] Dashboard ready.
    +
    +
+ + + + - \ No newline at end of file + diff --git a/frontend/login.html b/frontend/login.html index bba7306..402cb46 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -4,76 +4,268 @@ mxPIC EDA - Login - - - + + + - +
+ + +
+ + + + {% if error %} +
+ {{ error }} +
+ {% endif %} + + + + - \ No newline at end of file +