From 48555f568662a37855d0e2730bfaa494eb99d410 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 28 May 2026 11:56:02 +0800 Subject: [PATCH] Basic element added: anchor and port --- backend/__pycache__/database.cpython-39.pyc | Bin 1392 -> 5040 bytes backend/server.py | 18 +- .../mxpic_project_1/mxpic_project_1.yml | 75 ++ .../layout/mxpic_project_1/.project.json | 4 - .../engineer/layout/test_proj/.project.json | 4 - database/mxpic_data.db | Bin 20480 -> 28672 bytes frontend/canvas-helpers.js | 295 +++++++ frontend/canvas.html | 727 ++++++++++++++---- tests/canvas-generation-wiring.test.js | 55 ++ tests/canvas-helpers.test.js | 147 ++++ tests/canvas-static-route.test.js | 29 + 11 files changed, 1213 insertions(+), 141 deletions(-) create mode 100644 database/admin/layout/mxpic_project_1/mxpic_project_1.yml delete mode 100644 database/engineer/layout/mxpic_project_1/.project.json delete mode 100644 database/engineer/layout/test_proj/.project.json create mode 100644 frontend/canvas-helpers.js create mode 100644 tests/canvas-generation-wiring.test.js create mode 100644 tests/canvas-helpers.test.js create mode 100644 tests/canvas-static-route.test.js diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc index 2745305e26722fa9060ca4ce1eb95cbd6a3c1b66..9b21f2c707625e114badf988a1208cce6509cd09 100644 GIT binary patch literal 5040 zcmb_gOK;oQ73NDKEz6GWIL>2cS~hL!XsS4ArZbB)8INMosW7sfk>ySzWI)iov`vQ+ zm3yfsCgfGJC{PSA=&~p<@-o0I`VYG8Pbj$UDw}S)8MJ88b1o_BVJ9hyQ0C=*oclQE zeCKg(&CKKkc>eyorGwut3c}y8G5ToG_zYh5Cn$(OL{DhOU$H4dTk@oq+>}M^lRc%S zG!;>B#Dh#f%S)A)nlp7Q5`|<)_EnHq?S;^sCQ~E_-#nQndHBwd88QpsS#pNV z!S@WACkybMBWKAXeCNsgy@ezsm?RU?gkEQqTt|5f#zY)f8HRs9^G>TlR6!rxq(j)AH@tB<w0aop&Jh>Ta7Kf%N)w| zt0^hYaEV^2HH>>kUB6$iY?SH`_0NrmdTG0{S*gKV8%C{B9Giiww5^t-H;e}jeY;lq zV%y-m)V8bDandoPiRtk;NSH2J;5F-8(0$tX79LE`-+MQFm?P{DPBzZMIJc+MSa#t0 z?Q#CbSLsmy0PKyEjE~rk=bh3`oWOEV8*n?OMF@2mJ7tzl9q>xTwBns!qm}i|x>31T zLp@(b_CN>)y>6@K9=Wr)ekRoa2MkG`JDR0Nc3=Bt8x0w^zeyj883N-%# zuMl2x+u)GM&dP1iw=Iu-x{?xw#Zt8aNRGiD!@X24>ua0U?Twl~A)avcZPdgKSZW>v z-Roszy|i6z=r_XkH!k4YcDG}lghsl+vfB>8mX-@Lc+FIG;GL<1J4dyEyvfB7rW-ME43}7-ayf84!p{Myr_>^eQ{(JwWtsO3e61Plp;*Rg${Mww%hR>-D&T+ZO5VE#|?)CeD}c?EBbcgn{MfR zvfk70Cvp3ho}B&q|HkoPo8_i9sD@__x^35|!y~M2IS}oL9=z!!gXyOnrmzr6K8v)L z^%yixS)>47_9GSQ=way;@Xljql(oB*`IKTvjkH}bihaxTq6~!d7NauuX1jsi_Mb)? z3+OI}{)Enl3p3j_uMyGL?_n~Oc@p$64_e|UOx~(>A=34F^ z4Yy0gXU_bOF-@5z+;z zC-ou8>Iq2zhtvXkWf)a>Se3yWNwx5^1Q#wTa!{cHCb0ZQ8(Dps;i}@2p2gBh%G=7GmcnL5DUmYhJkm_{;gBl zAM;~xWR@1;Sd)C6P5H9A!M?e32%;cBcT%U)pTOv8312 z^<=oRWO~^^M*nq?Arg_lhFkJ^VOQf|Q2SX92DPuIa>>-fKcv~*WbjVX>BL$`y0inmsP~sZ^Xe~9jA0xN`t3#y7sbS=Wge&CdYZ&3%$fn; z-=%sSaa#YEqSA%Z)Q=+}?H;CqcufMkNk=y8WyrGcKE&H&+1Of32db5gN@IlZq*M6- zq!2rm3|Cf^@m$D8hB}B8xCgLF=Q8WzMVCVqFMr>_D&rwS&3>=sc!*equ}Cw`mgTlh zGrU|*{yCvzGU8gE8#+X1UE78`)Na@F4p$0F4W5EXZ^P8W5=G0T7jT*Rrw)A_I^De$ zKxmh`!68qjc)~?5A%$w~FrKt22fo{;?_nQoj&aO1hY1`{T6pm0A&jES=WdYu0*)9u zI5=h=F!wT&VEi8`_@zj@4gc=xdd{cls5l~#@4T3oFH2fhD`}cG4X>)Hxux8^2HC@Z E0VXTY6951J delta 829 zcmYjPO=}b}7*6IhGu_>OU{RssAksRuNVTn?h{ca8LhYs2!y*jRNwz!f?o3UxsLL*U zsDGf$&C?E^1P>m)h~UZJkdr^blLt?}nT2A8Ja68QID?eX9IU4;MbO;c>3{cwY7`&LuSS2R_(m3IV%kl! zrKWT9*4p~U?f#ttljVzH@bba--qvn#ZF42q44ZaU)!%qg%z6vOM{;2l`pOlrJE>%A z%1!btV~JGm3cXWLuKNqSu5T)FHmOL~$i(qb}c=NaP$+*aAgJk4a&N%C$tmC8;! zxnQBGEPWDpp`__aCS=r(Mf(%tb%^|3rFUOFO=ZN{abEnvi%V{rflK#2$cl3S2>Y0r zz6S{33?>G_e7YF63@!2iwXl{CJdk&2Le?pM`p-w9%PrW)^;2<2 zgQmub2HliT0fd$!J;8VucNwqgs4h~Lj#b1b(9l70z+|Ko0eC_eSCnz1BFyK&S0swM zahgR@zxX_Nb=0URC?gkY_6_4lebzg;AwX50%6Ahrt>aj>l^gGiYGEDbX{KD7R+RL4 zt+d0UXmW#T=yBFabB2We@&BW$)#ekv_kwOtdmVOPKbx2V(8|`#D+W1F2*D2i2WpDT A#{d8T 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 e270b38628a37e236184acda0b080b472db89d79..b4eb8c0c2095c05f30691019879a170d7a9f53c2 100644 GIT binary patch literal 28672 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 delta 64 zcmZp8z}T>Wae_1}HvqG^6M(&LXOZ3?o`M)yof88voaFc)H1R+6AMrLuw(&E&j OVkTaoLIwe-j3NLn`w+$e 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 @@ +