diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc index 2745305..9b21f2c 100644 Binary files a/backend/__pycache__/database.cpython-39.pyc and b/backend/__pycache__/database.cpython-39.pyc differ diff --git a/backend/server.py b/backend/server.py index 17e39c0..5b1c49d 100644 --- a/backend/server.py +++ b/backend/server.py @@ -6,7 +6,7 @@ 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 flask import Flask, jsonify, send_from_directory, request, redirect, url_for, session, render_template, make_response from werkzeug.security import check_password_hash import database from flask import Response @@ -36,6 +36,14 @@ app.json.sort_keys = False database.init_db() +def no_cache_response(response): + """Prevent stale editor assets while canvas features are being revised.""" + response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + return response + + def login_required_json(view_func): @wraps(view_func) def wrapper(*args, **kwargs): @@ -237,7 +245,13 @@ def canvas(): 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') + return no_cache_response(make_response(render_template('canvas.html'))) + + +@app.route('/canvas-helpers.js') +def canvas_helpers(): + """Serve the shared canvas helper script used by canvas.html.""" + return no_cache_response(send_from_directory(FRONTEND_DIR, 'canvas-helpers.js')) @app.route('/logout') def logout(): diff --git a/database/admin/layout/mxpic_project_1/mxpic_project_1.yml b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml new file mode 100644 index 0000000..94576c5 --- /dev/null +++ b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml @@ -0,0 +1,75 @@ +# ============================================= +# mxPIC Cell/Project Definition File +# ============================================= +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) +ports: +- name: port_3 + layer: WG_CORE + x: 359.0 + y: 447.0 + angle: 0.0 + width: 0.5 +- name: component_4 + layer: WG_CORE + x: 366.0 + y: 615.0 + angle: 0.0 + width: 0.5 + +# 2. Instances (The sub-components dropped onto this canvas) +instances: + component_2: + component: EMO1_2ML_CU_Al_RDL/composite/Mach_Zender_modulators/MZM_1600G_L3000_GSSG_TRAIL_TypeA2_QY_v1_20260303 + x: 799.0 + y: 420.0 + rotation: 0.0 + mirror: false + settings: + length: + +elements: + anchor_1: + type: anchor + x: 479.0 + y: 503.0 + angle: 0.0 + layer: WG_CORE + width: 0.5 + description: "" + port_3: + type: port + x: 359.0 + y: 447.0 + angle: 0.0 + layer: WG_CORE + width: 0.5 + description: "" + component_4: + type: port + x: 366.0 + y: 615.0 + angle: 0.0 + layer: WG_CORE + width: 0.5 + description: "" + +# 3. Bundles (Grouped links for multi-bus/parallel routing) +bundles: + output_bus: + routing_type: euler_bend + links: + - from: anchor_1:right + to: component_2:s1b + - from: anchor_1:left + to: port_3:port_3 + - from: component_2:s1b + to: component_2:s1b + - from: component_2:g2b + to: component_4:component_4 \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/.project.json b/database/engineer/layout/mxpic_project_1/.project.json deleted file mode 100644 index 4f868b9..0000000 --- a/database/engineer/layout/mxpic_project_1/.project.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "mxpic_project_1", - "technology": "Silterra/EMO1_2ML_CU_Al_RDL" -} \ No newline at end of file diff --git a/database/engineer/layout/test_proj/.project.json b/database/engineer/layout/test_proj/.project.json deleted file mode 100644 index 1a02b5f..0000000 --- a/database/engineer/layout/test_proj/.project.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test_proj", - "technology": "Silterra/EMO1_2ML_CU_Al_RDL" -} \ No newline at end of file diff --git a/database/mxpic_data.db b/database/mxpic_data.db index e270b38..b4eb8c0 100644 Binary files a/database/mxpic_data.db and b/database/mxpic_data.db differ diff --git a/frontend/canvas-helpers.js b/frontend/canvas-helpers.js new file mode 100644 index 0000000..18e2694 --- /dev/null +++ b/frontend/canvas-helpers.js @@ -0,0 +1,295 @@ +(function (root, factory) { + const helpers = factory(); + if (typeof module === 'object' && module.exports) { + module.exports = helpers; + } + root.MxpicCanvasHelpers = helpers; +})(typeof window !== 'undefined' ? window : globalThis, function () { + const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge'; + const FORGE_COMPONENT_TYPE = 'generate_with_forge'; + const ELEMENT_COMPONENTS = { + Port: { + name: 'Port', + elementType: 'port', + ports: { + port: { x: 0, y: 0, a: 0, width: 0.5 } + } + }, + Anchor: { + name: 'Anchor', + elementType: 'anchor', + ports: { + left: { x: -20, y: 0, a: 180, width: 0.5 }, + right: { x: 20, y: 0, a: 0, width: 0.5 } + } + } + }; + + const DEFAULT_FORGE_ARGUMENTS = { + function_name: 'straight', + component_name: '', + pdk: 'Silterra/EMO1_2ML_CU_Al_RDL', + layer: 'WG_CORE', + length: 100, + width: 0.5, + radius: 10, + gap: 0.2, + spacing: 10, + angle: 0, + wavelength: 1310, + port_count: 2, + include_heater: false, + include_electrical_ports: false, + notes: '' + }; + + const createForgeArguments = (overrides) => ({ + ...DEFAULT_FORGE_ARGUMENTS, + ...(overrides || {}) + }); + + const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE; + + const normalizeAngle = (angle) => { + const value = Number(angle); + if (!Number.isFinite(value)) return 0; + let normalized = ((value % 360) + 360) % 360; + if (normalized > 180) normalized -= 360; + return Object.is(normalized, -0) ? 0 : normalized; + }; + + const portSideFromAngle = (angle) => { + const normalized = normalizeAngle(angle); + if (normalized === 0) return 'right'; + if (normalized === 180 || normalized === -180) return 'left'; + if (normalized === 90) return 'top'; + if (normalized === -90) return 'bottom'; + return Math.abs(normalized) < 90 ? 'right' : 'left'; + }; + + const roundPercent = (value) => Number(value.toFixed(3)); + + const scaledPercent = (value, min, max, invert) => { + if (!Number.isFinite(value) || !Number.isFinite(min) || !Number.isFinite(max) || min === max) return null; + const ratio = (value - min) / (max - min); + const visualRatio = invert ? 1 - ratio : ratio; + return roundPercent(15 + visualRatio * 70); + }; + + const fallbackPercent = (index, count) => { + if (count <= 1) return 50; + return roundPercent(15 + (index / (count - 1)) * 70); + }; + + const buildSideHandles = (ports, side) => { + const vertical = side === 'left' || side === 'right'; + const coordinate = vertical ? 'y' : 'x'; + const values = ports.map(port => Number(port.info[coordinate])).filter(Number.isFinite); + const min = values.length ? Math.min(...values) : null; + const max = values.length ? Math.max(...values) : null; + + return ports.map((port, index) => { + const physicalPercent = scaledPercent(Number(port.info[coordinate]), min, max, vertical); + const percent = physicalPercent == null ? fallbackPercent(index, ports.length) : physicalPercent; + const percentValue = `${percent}%`; + const style = vertical + ? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' } + : { left: percentValue, transform: side === 'top' ? 'translate(-50%, -50%)' : 'translate(-50%, 50%)' }; + + return { + name: port.name, + position: side, + style, + port: port.info + }; + }); + }; + + const buildPortHandles = (ports) => { + const grouped = { left: [], right: [], top: [], bottom: [] }; + Object.entries(ports || {}).forEach(([name, info]) => { + if (name === 'a0' || name === 'b0') return; + const side = portSideFromAngle(info && info.a); + grouped[side].push({ name, info: info || {} }); + }); + + Object.values(grouped).forEach(sidePorts => { + sidePorts.sort((a, b) => { + const sideA = portSideFromAngle(a.info.a); + const vertical = sideA === 'left' || sideA === 'right'; + const primary = vertical ? Number(b.info.y || 0) - Number(a.info.y || 0) : Number(a.info.x || 0) - Number(b.info.x || 0); + return primary || a.name.localeCompare(b.name); + }); + }); + + return [ + ...buildSideHandles(grouped.left, 'left'), + ...buildSideHandles(grouped.right, 'right'), + ...buildSideHandles(grouped.top, 'top'), + ...buildSideHandles(grouped.bottom, 'bottom') + ]; + }; + + const toYamlScalar = (value) => { + if (value === null || value === undefined) return '""'; + if (typeof value === 'number' && Number.isFinite(value)) return String(value); + if (typeof value === 'boolean') return value ? 'true' : 'false'; + const numericValue = Number(value); + if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(numericValue) && String(numericValue) === value.trim()) { + return value.trim(); + } + return JSON.stringify(String(value)); + }; + + const buildSettingsYaml = (settings, indent) => { + const pad = ' '.repeat(indent); + const entries = Object.entries(settings || {}); + if (entries.length === 0) return `${pad}{}`; + return entries.map(([key, value]) => `${pad}${key}: ${toYamlScalar(value)}`).join('\n'); + }; + + const buildInstanceYaml = ({ instanceName, componentName, componentPath, position, rotation, forgeArguments }) => { + const forge = isForgeComponent(componentName); + const componentValue = forge ? FORGE_COMPONENT_TYPE : componentPath; + const settings = forge ? createForgeArguments(forgeArguments) : null; + const settingsYaml = forge ? `\n settings:\n${buildSettingsYaml(settings, 6)}` : '\n settings:\n length:'; + + return ` ${instanceName}: + component: ${componentValue} + x: ${Number(position.x || 0).toFixed(1)} + y: ${Number(position.y || 0).toFixed(1)} + rotation: ${Number(rotation || 0).toFixed(1)} + mirror: false${settingsYaml}`; + }; + + const buildInstancesYaml = ({ nodes, resolveComponentPath }) => { + return (nodes || []) + .filter(node => node.data && node.data.componentName && !node.data.elementType) + .map(node => { + const data = node.data; + const componentName = data.componentName || ''; + const componentPath = isForgeComponent(componentName) + ? FORGE_COMPONENT_TYPE + : (resolveComponentPath ? resolveComponentPath(componentName) : componentName); + + return buildInstanceYaml({ + instanceName: data.componentDisplayName || node.id, + componentName, + componentPath, + position: node.position || { x: 0, y: 0 }, + rotation: data.rotation || 0, + forgeArguments: data.forgeArguments + }); + }) + .join('\n\n'); + }; + + const getNodePortName = (node) => { + const name = node && node.data && (node.data.portName || node.data.componentDisplayName || node.data.label); + return name || (node && node.id) || 'port'; + }; + + const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode'); + const isElementNode = (node) => node && node.data && (node.data.elementType === 'port' || node.data.elementType === 'anchor'); + + const buildElementPorts = (elementType, data) => { + const element = ELEMENT_COMPONENTS[elementType === 'anchor' ? 'Anchor' : 'Port']; + if (!element) return {}; + if (element.elementType === 'port') { + return { + port: { + x: 0, + y: 0, + a: Number((data && (data.angle ?? data.a)) ?? 0), + width: Number((data && data.width) || 0.5) + } + }; + } + return JSON.parse(JSON.stringify(element.ports)); + }; + + const buildPageComponentPorts = (port, nodes) => { + const portNodes = (nodes || []).filter(isPortElementNode); + if (portNodes.length > 0) { + return portNodes.reduce((ports, node) => { + const data = node.data || {}; + ports[getNodePortName(node)] = { + x: Number((node.position && node.position.x) || 0), + y: Number((node.position && node.position.y) || 0), + a: Number(data.angle ?? data.a ?? 0), + width: Number(data.width || 0.5) + }; + return ports; + }, {}); + } + if (!port) return {}; + return { + port: { + x: Number(port.x || 0), + y: Number(port.y || 0), + a: Number(port.a || 0), + width: Number(port.width || 0.5) + } + }; + }; + + const buildCanvasPortsYaml = (nodes, fallbackPort) => { + const ports = buildPageComponentPorts(fallbackPort, nodes); + const entries = Object.entries(ports); + if (entries.length === 0) return 'ports: []'; + const sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node])); + const lines = entries.map(([name, info]) => { + const data = (sourceNodes.get(name) && sourceNodes.get(name).data) || {}; + const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : ''; + return `- name: ${name} + ${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'} + x: ${Number(info.x || 0).toFixed(1)} + y: ${Number(info.y || 0).toFixed(1)} + angle: ${Number(info.a || 0).toFixed(1)} + width: ${Number(info.width || 0.5)}${description}`; + }); + return `ports:\n${lines.join('\n')}`; + }; + + const buildPortsYaml = (port) => buildCanvasPortsYaml([], port); + + const buildElementsYaml = (nodes) => { + const elementNodes = (nodes || []).filter(isElementNode); + if (elementNodes.length === 0) return 'elements: {}'; + const lines = elementNodes.map(node => { + const data = node.data || {}; + const name = data.componentDisplayName || data.portName || node.id; + const angle = data.elementType === 'port' ? data.angle : data.rotation; + return ` ${name}: + type: ${data.elementType} + x: ${Number((node.position && node.position.x) || 0).toFixed(1)} + y: ${Number((node.position && node.position.y) || 0).toFixed(1)} + angle: ${Number(angle || 0).toFixed(1)} + layer: ${data.layer || 'WG_CORE'} + width: ${Number(data.width || 0.5)} + description: ${toYamlScalar(data.description || '')}`; + }); + return `elements:\n${lines.join('\n')}`; + }; + + return { + FORGE_COMPONENT_LABEL, + FORGE_COMPONENT_TYPE, + ELEMENT_COMPONENTS, + DEFAULT_FORGE_ARGUMENTS, + createForgeArguments, + isForgeComponent, + normalizeAngle, + portSideFromAngle, + buildPortHandles, + buildElementPorts, + buildInstanceYaml, + buildInstancesYaml, + buildPageComponentPorts, + buildCanvasPortsYaml, + buildPortsYaml, + buildElementsYaml, + buildSettingsYaml, + toYamlScalar + }; +}); diff --git a/frontend/canvas.html b/frontend/canvas.html index 96845f3..d40586d 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -15,6 +15,7 @@ +