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 @@
+