Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2846899097 | |||
| 2ddd30e7bb | |||
| 866bc1de18 | |||
| 23d631c4f0 | |||
| af5134dcee | |||
| feed2e0576 | |||
| 960066735c | |||
| 9b4f43f0b1 | |||
| cf28676756 | |||
| 09cadc7430 | |||
| 13d42af90d |
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-19
@@ -1,6 +1,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints.
|
||||
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, file_version, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
||||
# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta
|
||||
# Developer : Qin Yue @ 2026
|
||||
# Organization : OptiHK Limited
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -9,7 +9,6 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import json
|
||||
import uuid
|
||||
import yaml
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
@@ -136,12 +135,6 @@ def cell_svg_path(project_name, cell_name):
|
||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg")
|
||||
|
||||
|
||||
def file_version(path):
|
||||
"""Return a cache-busting version token for a completed file."""
|
||||
stat = os.stat(path)
|
||||
return f"{stat.st_mtime_ns}-{stat.st_size}"
|
||||
|
||||
|
||||
def cell_routes_path(project_name, cell_name):
|
||||
"""Return the route sidecar JSON path for a project cell."""
|
||||
return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.routes.yml")
|
||||
@@ -746,31 +739,24 @@ def save_layout():
|
||||
write_route_points_sidecar(content, cell_routes_path(project, cell))
|
||||
|
||||
svg_path = None
|
||||
svg_version = None
|
||||
preview_status = "not_requested"
|
||||
preview_error = None
|
||||
if create_preview:
|
||||
svg_path = cell_svg_path(project, cell)
|
||||
temp_svg_path = f"{svg_path}.building-{os.getpid()}-{uuid.uuid4().hex}.svg"
|
||||
try:
|
||||
create_routed_layout_svg(
|
||||
content,
|
||||
temp_svg_path,
|
||||
svg_path,
|
||||
pdk_root=current_pdk_root(),
|
||||
project_dir=project_root(project),
|
||||
technology_manifest_path=technology_manifest_path_for_project(project),
|
||||
prefer_full_gds=prefer_full_gds_for_session(session),
|
||||
)
|
||||
os.replace(temp_svg_path, svg_path)
|
||||
svg_version = file_version(svg_path)
|
||||
preview_status = "generated"
|
||||
except RouterStackUnavailable as e:
|
||||
preview_status = "skipped"
|
||||
preview_error = str(e)
|
||||
svg_path = None
|
||||
finally:
|
||||
if os.path.exists(temp_svg_path):
|
||||
os.remove(temp_svg_path)
|
||||
|
||||
record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path})
|
||||
return jsonify({
|
||||
@@ -779,9 +765,7 @@ def save_layout():
|
||||
"cell": cell,
|
||||
"path": save_path,
|
||||
"svg_path": svg_path,
|
||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version) if svg_path else None,
|
||||
"svg_ready": bool(svg_path and svg_version),
|
||||
"svg_version": svg_version,
|
||||
"svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None,
|
||||
"preview_status": preview_status,
|
||||
"preview_error": preview_error
|
||||
}), 200
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 319 KiB |
@@ -1,211 +0,0 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 500
|
||||
height: 600
|
||||
project: mxpic_project_1
|
||||
name: canvas_1
|
||||
type: composite
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
pins:
|
||||
- name: port_io1
|
||||
layer: WG_CORE
|
||||
element: port
|
||||
pin: io1
|
||||
x: 40.0
|
||||
y: -90.0
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
- name: port_1_io1
|
||||
layer: WG_CORE
|
||||
element: port_1
|
||||
pin: io1
|
||||
x: 410.0
|
||||
y: -35.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
- name: port_1_io2
|
||||
layer: WG_CORE
|
||||
element: port_1
|
||||
pin: io2
|
||||
x: 410.0
|
||||
y: -25.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
- name: port_2_io1
|
||||
layer: WG_CORE
|
||||
element: port_2
|
||||
pin: io1
|
||||
x: 390.0
|
||||
y: -215.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
- name: port_2_io2
|
||||
layer: WG_CORE
|
||||
element: port_2
|
||||
pin: io2
|
||||
x: 390.0
|
||||
y: -205.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
MMI_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 130.0
|
||||
y: -90.0
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
MMI_2:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 280.0
|
||||
y: -30.0
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
MMI_3:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 320.1
|
||||
y: -144.7
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
port:
|
||||
type: port
|
||||
x: 40.0
|
||||
y: -90.0
|
||||
angle: 180.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_io1
|
||||
role: io1
|
||||
port:
|
||||
type: port
|
||||
x: 40.0
|
||||
y: -90.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_io1
|
||||
role: io1
|
||||
port_1:
|
||||
type: port
|
||||
x: 410.0
|
||||
y: -30.0
|
||||
angle: 180.0
|
||||
pin_number: 2
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_1_io1
|
||||
role: io1
|
||||
- name: port_1_io2
|
||||
role: io2
|
||||
port_2:
|
||||
type: port
|
||||
x: 390.0
|
||||
y: -210.0
|
||||
angle: 180.0
|
||||
pin_number: 2
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_2_io1
|
||||
role: io1
|
||||
- name: port_2_io2
|
||||
role: io2
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: MMI_1:a1
|
||||
to: port:port_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:a1
|
||||
to: MMI_1:b1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:a1
|
||||
to: MMI_1:b2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:b1
|
||||
to: port_1:port_1_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:b1
|
||||
to: port_1:port_1_io2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:b2
|
||||
to: port_1:port_1_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:b1
|
||||
to: port_2:port_2_io2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:b2
|
||||
to: port_2:port_2_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,100 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 5000
|
||||
height: 5000
|
||||
project: mxpic_project_1
|
||||
name: canvas_1
|
||||
type: composite
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
pins:
|
||||
- name: port_io1
|
||||
layer: WG_CORE
|
||||
element: port
|
||||
pin: io1
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
waveguide_1:
|
||||
component: waveguide
|
||||
x: 686.5
|
||||
y: -1027.9
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length: 100
|
||||
width: 0.5
|
||||
xsection: "strip"
|
||||
|
||||
circle_1:
|
||||
component: circle
|
||||
x: 877.2
|
||||
y: -1093.7
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
radius: 10
|
||||
width: 0.5
|
||||
xsection: "strip"
|
||||
|
||||
waveguide_2:
|
||||
component: waveguide
|
||||
x: 858.0
|
||||
y: -1029.6
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length: 100
|
||||
width: 0.5
|
||||
xsection: "strip"
|
||||
|
||||
elements:
|
||||
port:
|
||||
type: port
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_io1
|
||||
role: io1
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: waveguide_1:b1
|
||||
to: waveguide_2:a1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.5
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: waveguide_2:b1
|
||||
to: circle_1:a1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.5
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6516.95" height="2377.45" viewBox="14124.775 14328.775 6516.95 2377.45">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.l1111d0 {stroke: #B3446C; fill: #B3446C; fill-opacity: 0.5;}
|
||||
</style>
|
||||
</defs>
|
||||
<rect x="14124.775" y="14328.775" width="6516.95" height="2377.45" fill="#222222" stroke="none"/>
|
||||
<g id="canvas_2" transform="scale(1 -1)">
|
||||
<polygon id="00000215A6721690" class="l1111d0" points="20243,-14702.5 14421,-14702.5 14421,-14697.5 20243,-14697.5"/>
|
||||
<polygon id="00000215A67222D0" class="l1111d0" points="20340.5,-14800 20345.5,-14800 20345.5,-14798.61 20345.42,-14795.84 20345.27,-14793.06 20345.05,-14790.3 20344.75,-14787.54 20344.37,-14784.79 20343.92,-14782.05 20343.4,-14779.32 20342.8,-14776.61 20342.13,-14773.92 20341.39,-14771.24 20340.57,-14768.59 20339.69,-14765.96 20338.73,-14763.35 20337.7,-14760.77 20336.61,-14758.22 20335.44,-14755.7 20334.21,-14753.22 20332.91,-14750.76 20331.54,-14748.35 20330.11,-14745.97 20328.61,-14743.63 20327.06,-14741.33 20325.44,-14739.08 20323.76,-14736.87 20322.02,-14734.7 20320.22,-14732.59 20318.37,-14730.52 20316.46,-14728.51 20314.49,-14726.54 20312.48,-14724.63 20310.41,-14722.78 20308.3,-14720.98 20306.13,-14719.24 20303.92,-14717.56 20301.67,-14715.94 20299.37,-14714.39 20297.03,-14712.89 20294.65,-14711.46 20292.24,-14710.09 20289.78,-14708.79 20287.3,-14707.56 20284.78,-14706.39 20282.23,-14705.3 20279.65,-14704.27 20277.04,-14703.31 20274.41,-14702.43 20271.76,-14701.61 20269.08,-14700.87 20266.39,-14700.2 20263.68,-14699.6 20260.95,-14699.08 20258.21,-14698.63 20255.46,-14698.25 20252.7,-14697.95 20249.94,-14697.73 20247.16,-14697.58 20244.39,-14697.5 20243,-14697.5 20243,-14702.5 20244.37,-14702.5 20247.1,-14702.58 20249.83,-14702.73 20252.56,-14702.96 20255.28,-14703.27 20257.98,-14703.65 20260.68,-14704.11 20263.36,-14704.64 20266.03,-14705.25 20268.68,-14705.94 20271.3,-14706.69 20273.91,-14707.52 20276.49,-14708.43 20279.05,-14709.4 20281.57,-14710.45 20284.07,-14711.57 20286.53,-14712.75 20288.96,-14714.01 20291.36,-14715.33 20293.71,-14716.72 20296.03,-14718.17 20298.3,-14719.69 20300.53,-14721.28 20302.72,-14722.92 20304.86,-14724.63 20306.95,-14726.39 20308.99,-14728.21 20310.97,-14730.09 20312.91,-14732.03 20314.79,-14734.01 20316.61,-14736.05 20318.37,-14738.14 20320.08,-14740.28 20321.72,-14742.47 20323.31,-14744.7 20324.83,-14746.97 20326.28,-14749.29 20327.67,-14751.64 20328.99,-14754.04 20330.25,-14756.47 20331.43,-14758.93 20332.55,-14761.43 20333.6,-14763.95 20334.57,-14766.51 20335.48,-14769.09 20336.31,-14771.7 20337.06,-14774.32 20337.75,-14776.97 20338.36,-14779.64 20338.89,-14782.32 20339.35,-14785.02 20339.73,-14787.72 20340.04,-14790.44 20340.27,-14793.17 20340.42,-14795.9 20340.5,-14798.63"/>
|
||||
<polygon id="00000215A67218C0" class="l1111d0" points="20340.5,-15990 20340.5,-14800 20345.5,-14800 20345.5,-15990"/>
|
||||
<polygon id="00000215A6722030" class="l1111d0" points="20243,-16087.5 20243,-16092.5 20244.39,-16092.5 20247.16,-16092.42 20249.94,-16092.27 20252.7,-16092.05 20255.46,-16091.75 20258.21,-16091.37 20260.95,-16090.92 20263.68,-16090.4 20266.39,-16089.8 20269.08,-16089.13 20271.76,-16088.39 20274.41,-16087.57 20277.04,-16086.69 20279.65,-16085.73 20282.23,-16084.7 20284.78,-16083.61 20287.3,-16082.44 20289.78,-16081.21 20292.24,-16079.91 20294.65,-16078.54 20297.03,-16077.11 20299.37,-16075.61 20301.67,-16074.06 20303.92,-16072.44 20306.13,-16070.76 20308.3,-16069.02 20310.41,-16067.22 20312.48,-16065.37 20314.49,-16063.46 20316.46,-16061.49 20318.37,-16059.48 20320.22,-16057.41 20322.02,-16055.3 20323.76,-16053.13 20325.44,-16050.92 20327.06,-16048.67 20328.61,-16046.37 20330.11,-16044.03 20331.54,-16041.65 20332.91,-16039.24 20334.21,-16036.78 20335.44,-16034.3 20336.61,-16031.78 20337.7,-16029.23 20338.73,-16026.65 20339.69,-16024.04 20340.57,-16021.41 20341.39,-16018.76 20342.13,-16016.08 20342.8,-16013.39 20343.4,-16010.68 20343.92,-16007.95 20344.37,-16005.21 20344.75,-16002.46 20345.05,-15999.7 20345.27,-15996.94 20345.42,-15994.16 20345.5,-15991.39 20345.5,-15990 20340.5,-15990 20340.5,-15991.37 20340.42,-15994.1 20340.27,-15996.83 20340.04,-15999.56 20339.73,-16002.28 20339.35,-16004.98 20338.89,-16007.68 20338.36,-16010.36 20337.75,-16013.03 20337.06,-16015.68 20336.31,-16018.3 20335.48,-16020.91 20334.57,-16023.49 20333.6,-16026.05 20332.55,-16028.57 20331.43,-16031.07 20330.25,-16033.53 20328.99,-16035.96 20327.67,-16038.36 20326.28,-16040.71 20324.83,-16043.03 20323.31,-16045.3 20321.72,-16047.53 20320.08,-16049.72 20318.37,-16051.86 20316.61,-16053.95 20314.79,-16055.99 20312.91,-16057.97 20310.97,-16059.91 20308.99,-16061.79 20306.95,-16063.61 20304.86,-16065.37 20302.72,-16067.08 20300.53,-16068.72 20298.3,-16070.31 20296.03,-16071.83 20293.71,-16073.28 20291.36,-16074.67 20288.96,-16075.99 20286.53,-16077.25 20284.07,-16078.43 20281.57,-16079.55 20279.05,-16080.6 20276.49,-16081.57 20273.91,-16082.48 20271.3,-16083.31 20268.68,-16084.06 20266.03,-16084.75 20263.36,-16085.36 20260.68,-16085.89 20257.98,-16086.35 20255.28,-16086.73 20252.56,-16087.04 20249.83,-16087.27 20247.1,-16087.42 20244.37,-16087.5"/>
|
||||
<polygon id="00000215A6721A80" class="l1111d0" points="16810,-14775 14421,-14775 14421,-14625 16810,-14625"/>
|
||||
<polygon id="00000215A6720F90" class="l1111d0" points="16835,-14800 16985,-14800 16985,-14798.17 16984.92,-14794.5 16984.77,-14790.84 16984.54,-14787.18 16984.23,-14783.53 16983.85,-14779.89 16983.39,-14776.25 16982.85,-14772.62 16982.24,-14769.01 16981.55,-14765.41 16980.79,-14761.82 16979.95,-14758.26 16979.04,-14754.71 16978.06,-14751.17 16977,-14747.67 16975.87,-14744.18 16974.66,-14740.72 16973.38,-14737.28 16972.03,-14733.88 16970.61,-14730.5 16969.12,-14727.15 16967.56,-14723.83 16965.93,-14720.55 16964.23,-14717.3 16962.47,-14714.09 16960.64,-14710.91 16958.74,-14707.78 16956.77,-14704.68 16954.74,-14701.63 16952.65,-14698.62 16950.5,-14695.66 16948.28,-14692.74 16946.01,-14689.86 16943.67,-14687.04 16941.27,-14684.27 16938.82,-14681.54 16936.31,-14678.87 16933.75,-14676.25 16931.13,-14673.69 16928.46,-14671.18 16925.73,-14668.73 16922.96,-14666.33 16920.14,-14663.99 16917.26,-14661.72 16914.34,-14659.5 16911.38,-14657.35 16908.37,-14655.26 16905.32,-14653.23 16902.22,-14651.26 16899.09,-14649.36 16895.91,-14647.53 16892.7,-14645.77 16889.45,-14644.07 16886.17,-14642.44 16882.85,-14640.88 16879.5,-14639.39 16876.12,-14637.97 16872.72,-14636.62 16869.28,-14635.34 16865.82,-14634.13 16862.33,-14633 16858.83,-14631.94 16855.29,-14630.96 16851.74,-14630.05 16848.18,-14629.21 16844.59,-14628.45 16840.99,-14627.76 16837.38,-14627.15 16833.75,-14626.61 16830.11,-14626.15 16826.47,-14625.77 16822.82,-14625.46 16819.16,-14625.23 16815.5,-14625.08 16811.83,-14625 16810,-14625 16810,-14775 16810.68,-14775 16812.03,-14775.08 16813.38,-14775.22 16814.71,-14775.44 16816.03,-14775.73 16817.34,-14776.1 16818.62,-14776.53 16819.88,-14777.03 16821.11,-14777.6 16822.31,-14778.23 16823.47,-14778.93 16824.59,-14779.69 16825.67,-14780.51 16826.7,-14781.39 16827.68,-14782.32 16828.61,-14783.3 16829.49,-14784.33 16830.31,-14785.41 16831.07,-14786.53 16831.77,-14787.69 16832.4,-14788.89 16832.97,-14790.12 16833.47,-14791.38 16833.9,-14792.66 16834.27,-14793.97 16834.56,-14795.29 16834.78,-14796.62 16834.92,-14797.97 16835,-14799.32"/>
|
||||
<polygon id="00000215A6721930" class="l1111d0" points="16835,-16235 16835,-14800 16985,-14800 16985,-16235"/>
|
||||
<polygon id="00000215A67214D0" class="l1111d0" points="16810,-16260 16810,-16410 16811.83,-16410 16815.5,-16409.92 16819.16,-16409.77 16822.82,-16409.54 16826.47,-16409.23 16830.11,-16408.85 16833.75,-16408.39 16837.38,-16407.85 16840.99,-16407.24 16844.59,-16406.55 16848.18,-16405.79 16851.74,-16404.95 16855.29,-16404.04 16858.83,-16403.06 16862.33,-16402 16865.82,-16400.87 16869.28,-16399.66 16872.72,-16398.38 16876.12,-16397.03 16879.5,-16395.61 16882.85,-16394.12 16886.17,-16392.56 16889.45,-16390.93 16892.7,-16389.23 16895.91,-16387.47 16899.09,-16385.64 16902.22,-16383.74 16905.32,-16381.77 16908.37,-16379.74 16911.38,-16377.65 16914.34,-16375.5 16917.26,-16373.28 16920.14,-16371.01 16922.96,-16368.67 16925.73,-16366.27 16928.46,-16363.82 16931.13,-16361.31 16933.75,-16358.75 16936.31,-16356.13 16938.82,-16353.46 16941.27,-16350.73 16943.67,-16347.96 16946.01,-16345.14 16948.28,-16342.26 16950.5,-16339.34 16952.65,-16336.38 16954.74,-16333.37 16956.77,-16330.32 16958.74,-16327.22 16960.64,-16324.09 16962.47,-16320.91 16964.23,-16317.7 16965.93,-16314.45 16967.56,-16311.17 16969.12,-16307.85 16970.61,-16304.5 16972.03,-16301.12 16973.38,-16297.72 16974.66,-16294.28 16975.87,-16290.82 16977,-16287.33 16978.06,-16283.83 16979.04,-16280.29 16979.95,-16276.74 16980.79,-16273.18 16981.55,-16269.59 16982.24,-16265.99 16982.85,-16262.38 16983.39,-16258.75 16983.85,-16255.11 16984.23,-16251.47 16984.54,-16247.82 16984.77,-16244.16 16984.92,-16240.5 16985,-16236.83 16985,-16235 16835,-16235 16835,-16235.68 16834.92,-16237.03 16834.78,-16238.38 16834.56,-16239.71 16834.27,-16241.03 16833.9,-16242.34 16833.47,-16243.62 16832.97,-16244.88 16832.4,-16246.11 16831.77,-16247.31 16831.07,-16248.47 16830.31,-16249.59 16829.49,-16250.67 16828.61,-16251.7 16827.68,-16252.68 16826.7,-16253.61 16825.67,-16254.49 16824.59,-16255.31 16823.47,-16256.07 16822.31,-16256.77 16821.11,-16257.4 16819.88,-16257.97 16818.62,-16258.47 16817.34,-16258.9 16816.03,-16259.27 16814.71,-16259.56 16813.38,-16259.78 16812.03,-16259.92 16810.68,-16260"/>
|
||||
<polygon id="00000215A67220A0" class="l1111d0" points="15610,-14775 14421,-14775 14421,-14625 15610,-14625"/>
|
||||
<polygon id="00000215A6722650" class="l1111d0" points="15635,-14800 15785,-14800 15785,-14798.17 15784.92,-14794.5 15784.77,-14790.84 15784.54,-14787.18 15784.23,-14783.53 15783.85,-14779.89 15783.39,-14776.25 15782.85,-14772.62 15782.24,-14769.01 15781.55,-14765.41 15780.79,-14761.82 15779.95,-14758.26 15779.04,-14754.71 15778.06,-14751.17 15777,-14747.67 15775.87,-14744.18 15774.66,-14740.72 15773.38,-14737.28 15772.03,-14733.88 15770.61,-14730.5 15769.12,-14727.15 15767.56,-14723.83 15765.93,-14720.55 15764.23,-14717.3 15762.47,-14714.09 15760.64,-14710.91 15758.74,-14707.78 15756.77,-14704.68 15754.74,-14701.63 15752.65,-14698.62 15750.5,-14695.66 15748.28,-14692.74 15746.01,-14689.86 15743.67,-14687.04 15741.27,-14684.27 15738.82,-14681.54 15736.31,-14678.87 15733.75,-14676.25 15731.13,-14673.69 15728.46,-14671.18 15725.73,-14668.73 15722.96,-14666.33 15720.14,-14663.99 15717.26,-14661.72 15714.34,-14659.5 15711.38,-14657.35 15708.37,-14655.26 15705.32,-14653.23 15702.22,-14651.26 15699.09,-14649.36 15695.91,-14647.53 15692.7,-14645.77 15689.45,-14644.07 15686.17,-14642.44 15682.85,-14640.88 15679.5,-14639.39 15676.12,-14637.97 15672.72,-14636.62 15669.28,-14635.34 15665.82,-14634.13 15662.33,-14633 15658.83,-14631.94 15655.29,-14630.96 15651.74,-14630.05 15648.18,-14629.21 15644.59,-14628.45 15640.99,-14627.76 15637.38,-14627.15 15633.75,-14626.61 15630.11,-14626.15 15626.47,-14625.77 15622.82,-14625.46 15619.16,-14625.23 15615.5,-14625.08 15611.83,-14625 15610,-14625 15610,-14775 15610.68,-14775 15612.03,-14775.08 15613.38,-14775.22 15614.71,-14775.44 15616.03,-14775.73 15617.34,-14776.1 15618.62,-14776.53 15619.88,-14777.03 15621.11,-14777.6 15622.31,-14778.23 15623.47,-14778.93 15624.59,-14779.69 15625.67,-14780.51 15626.7,-14781.39 15627.68,-14782.32 15628.61,-14783.3 15629.49,-14784.33 15630.31,-14785.41 15631.07,-14786.53 15631.77,-14787.69 15632.4,-14788.89 15632.97,-14790.12 15633.47,-14791.38 15633.9,-14792.66 15634.27,-14793.97 15634.56,-14795.29 15634.78,-14796.62 15634.92,-14797.97 15635,-14799.32"/>
|
||||
<polygon id="00000215A67226C0" class="l1111d0" points="15635,-16235 15635,-14800 15785,-14800 15785,-16235"/>
|
||||
<polygon id="00000215A6721FC0" class="l1111d0" points="15810,-16260 15810,-16410 15808.17,-16410 15804.5,-16409.92 15800.84,-16409.77 15797.18,-16409.54 15793.53,-16409.23 15789.89,-16408.85 15786.25,-16408.39 15782.62,-16407.85 15779.01,-16407.24 15775.41,-16406.55 15771.82,-16405.79 15768.26,-16404.95 15764.71,-16404.04 15761.17,-16403.06 15757.67,-16402 15754.18,-16400.87 15750.72,-16399.66 15747.28,-16398.38 15743.88,-16397.03 15740.5,-16395.61 15737.15,-16394.12 15733.83,-16392.56 15730.55,-16390.93 15727.3,-16389.23 15724.09,-16387.47 15720.91,-16385.64 15717.78,-16383.74 15714.68,-16381.77 15711.63,-16379.74 15708.62,-16377.65 15705.66,-16375.5 15702.74,-16373.28 15699.86,-16371.01 15697.04,-16368.67 15694.27,-16366.27 15691.54,-16363.82 15688.87,-16361.31 15686.25,-16358.75 15683.69,-16356.13 15681.18,-16353.46 15678.73,-16350.73 15676.33,-16347.96 15673.99,-16345.14 15671.72,-16342.26 15669.5,-16339.34 15667.35,-16336.38 15665.26,-16333.37 15663.23,-16330.32 15661.26,-16327.22 15659.36,-16324.09 15657.53,-16320.91 15655.77,-16317.7 15654.07,-16314.45 15652.44,-16311.17 15650.88,-16307.85 15649.39,-16304.5 15647.97,-16301.12 15646.62,-16297.72 15645.34,-16294.28 15644.13,-16290.82 15643,-16287.33 15641.94,-16283.83 15640.96,-16280.29 15640.05,-16276.74 15639.21,-16273.18 15638.45,-16269.59 15637.76,-16265.99 15637.15,-16262.38 15636.61,-16258.75 15636.15,-16255.11 15635.77,-16251.47 15635.46,-16247.82 15635.23,-16244.16 15635.08,-16240.5 15635,-16236.83 15635,-16235 15785,-16235 15785,-16235.68 15785.08,-16237.03 15785.22,-16238.38 15785.44,-16239.71 15785.73,-16241.03 15786.1,-16242.34 15786.53,-16243.62 15787.03,-16244.88 15787.6,-16246.11 15788.23,-16247.31 15788.93,-16248.47 15789.69,-16249.59 15790.51,-16250.67 15791.39,-16251.7 15792.32,-16252.68 15793.3,-16253.61 15794.33,-16254.49 15795.41,-16255.31 15796.53,-16256.07 15797.69,-16256.77 15798.89,-16257.4 15800.12,-16257.97 15801.38,-16258.47 15802.66,-16258.9 15803.97,-16259.27 15805.29,-16259.56 15806.62,-16259.78 15807.97,-16259.92 15809.32,-16260"/>
|
||||
<polygon id="00000215A6722110" class="l1111d0" points="15810,-16260 16810,-16260 16810,-16410 15810,-16410"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,136 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 5000
|
||||
height: 5000
|
||||
project: mxpic_project_1
|
||||
name: canvas_2
|
||||
type: composite
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
pins:
|
||||
- name: port_io1
|
||||
layer: WG_CORE
|
||||
element: port
|
||||
pin: io1
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
- name: port_2_io1
|
||||
layer: WG_CORE
|
||||
element: port_2
|
||||
pin: io1
|
||||
x: 1442.1
|
||||
y: -1470.0
|
||||
angle: 180.0
|
||||
width: 10
|
||||
- name: port_3_io1
|
||||
layer: WG_CORE
|
||||
element: port_3
|
||||
pin: io1
|
||||
x: 2024.3
|
||||
y: -1609.0
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
waveguide_3:
|
||||
component: waveguide
|
||||
x: 1581.0
|
||||
y: -1633.5
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length: 100
|
||||
width: 15
|
||||
xsection: "strip"
|
||||
|
||||
elements:
|
||||
port:
|
||||
type: port
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_io1
|
||||
role: io1
|
||||
port:
|
||||
type: port
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 180.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_io1
|
||||
role: io1
|
||||
port_2:
|
||||
type: port
|
||||
x: 1442.1
|
||||
y: -1470.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 10
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_2_io1
|
||||
role: io1
|
||||
port_3:
|
||||
type: port
|
||||
x: 2024.3
|
||||
y: -1609.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_3_io1
|
||||
role: io1
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: waveguide_3:a1
|
||||
to: port_2:port_2_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 15
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: waveguide_3:b1
|
||||
to: port_2:port_2_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 15
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: port_3:port_3_io1
|
||||
to: port_2:port_2_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.5
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 15 KiB |
@@ -13,14 +13,79 @@ type: project
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
ports: []
|
||||
pins:
|
||||
- name: port_1_io1
|
||||
layer: WG_CORE
|
||||
element: port_1
|
||||
pin: io1
|
||||
x: 1699.6
|
||||
y: -1844.2
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
circle_1:
|
||||
component: circle
|
||||
x: 1877.6
|
||||
y: -1816.7
|
||||
rotation: 90.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
radius: 10
|
||||
width: 0.5
|
||||
xsection: "strip"
|
||||
|
||||
BD_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/bendings/SiN_EUB_1310_H400_w2500_L45_QY_202604
|
||||
x: 1926.2
|
||||
y: -1813.9
|
||||
rotation: 90.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
DC_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/directional_couplers/DC_SiN400_99_1_1310_jyh_quantex_202603
|
||||
x: 1766.5
|
||||
y: -1945.3
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
MZM_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZI_SiN400_Si220_PIN_mod_1310_L1300_QY_202603
|
||||
x: 1341.6
|
||||
y: -2103.8
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
phase_shifter_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/phase_shifters/HT_150R_SiPPP_L500_100OHM_DUMMY_QY_202604
|
||||
x: 2695.0
|
||||
y: -2275.0
|
||||
rotation: 90.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
MMI_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 1511.5
|
||||
y: -2531.5
|
||||
x: 2849.0
|
||||
y: -1988.6
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
@@ -28,10 +93,10 @@ instances:
|
||||
settings:
|
||||
length:
|
||||
|
||||
MMI_2:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 1716.4
|
||||
y: -2293.8
|
||||
DC_2:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/directional_couplers/DC_SiN400_99_1_1310_jyh_quantex_202603
|
||||
x: 2656.9
|
||||
y: -1992.8
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
@@ -39,17 +104,34 @@ instances:
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements: {}
|
||||
PD_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/photodetectors/PD_1310_Monitor_Si220_Ge500_NPN_XHN_202604
|
||||
x: 3151.7
|
||||
y: -2032.1
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
port_1:
|
||||
type: port
|
||||
x: 1699.6
|
||||
y: -1844.2
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_1_io1
|
||||
role: io1
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: MMI_2:a1
|
||||
to: MMI_1:b2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
Binary file not shown.
@@ -42,11 +42,14 @@
|
||||
name: 'Anchor',
|
||||
elementType: 'anchor',
|
||||
ports: {
|
||||
a1: { x: 0, y: -PORT_NODE_SIZE / 2, a: 180, width: 0.5 },
|
||||
b1: { x: 0, y: -PORT_NODE_SIZE / 2, a: 0, width: 0.5 }
|
||||
a1: { x: 0, y: 0, a: 180, width: 0.5 },
|
||||
b1: { x: 0, y: 0, a: 0, width: 0.5 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Defines local primitive components that do not require PDK lookup.
|
||||
const BASIC_COMPONENTS = {
|
||||
waveguide: {
|
||||
@@ -804,7 +807,6 @@
|
||||
}
|
||||
};
|
||||
}
|
||||
if (portNumber > 1) {
|
||||
const entries = [];
|
||||
Array.from({ length: portNumber }, (_, index) => {
|
||||
const y = elementPortOffset(index, portNumber, pitch);
|
||||
@@ -812,8 +814,6 @@
|
||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
||||
});
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(element.ports));
|
||||
};
|
||||
|
||||
// Generate port metadata for built-in primitive components.
|
||||
@@ -984,6 +984,20 @@ ${pinLines}`;
|
||||
return `elements:\n${lines.join('\n')}`;
|
||||
};
|
||||
|
||||
const finiteNumberOrNull = (value) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
};
|
||||
|
||||
const getRouteEndpointWidth = (node, handleId) => {
|
||||
if (!node || !node.data) return null;
|
||||
const dataWidth = finiteNumberOrNull(node.data.width);
|
||||
if (dataWidth !== null) return dataWidth;
|
||||
const ports = node.data.ports || {};
|
||||
const portWidth = ports[handleId] ? finiteNumberOrNull(ports[handleId].width) : null;
|
||||
return portWidth;
|
||||
};
|
||||
|
||||
// Serialize canvas links into routed bundle YAML including route settings and bend points.
|
||||
const buildBundlesYaml = (page, manifest) => {
|
||||
const { nodes = [], edges = [] } = page || {};
|
||||
@@ -1004,6 +1018,9 @@ ${pinLines}`;
|
||||
? getElementPinName(targetNode, edge.targetHandle)
|
||||
: edge.targetHandle || 'unknown';
|
||||
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||
const routeWidth = getRouteEndpointWidth(sourceNode, edge.sourceHandle)
|
||||
?? getRouteEndpointWidth(targetNode, edge.targetHandle)
|
||||
?? route.width;
|
||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
||||
const pointsYaml = points.length > 0
|
||||
@@ -1014,7 +1031,7 @@ ${pinLines}`;
|
||||
return ` - id: ${toYamlScalar(edge.id)}
|
||||
xsection: ${route.xsection}
|
||||
family: ${route.family}
|
||||
width: ${Number(route.width)}
|
||||
width: ${Number(routeWidth)}
|
||||
radius: ${Number(route.radius)}
|
||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||
}
|
||||
@@ -1022,7 +1039,7 @@ ${pinLines}`;
|
||||
to: ${targetName}:${toPort}
|
||||
xsection: ${route.xsection}
|
||||
family: ${route.family}
|
||||
width: ${Number(route.width)}
|
||||
width: ${Number(routeWidth)}
|
||||
radius: ${Number(route.radius)}
|
||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||
});
|
||||
@@ -1220,6 +1237,22 @@ ${linksYaml}`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRotatableNodeHandleDirection = (node, handleId) => {
|
||||
if (!node || !handleId) return null;
|
||||
if (node.type !== 'rotatableNode' && !(!node.data?.elementType && node.data?.componentName)) return null;
|
||||
const ports = node.data && node.data.ports;
|
||||
if (!ports || !ports[handleId]) return null;
|
||||
const boxSize = normalizeBoxSize({ box_size: node.data && node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||
const handles = buildPortHandles(ports, {
|
||||
rotation: Number((node.data && node.data.rotation) || 0),
|
||||
flip: Boolean(node.data && node.data.flip),
|
||||
flop: Boolean(node.data && node.data.flop),
|
||||
boxSize
|
||||
});
|
||||
const found = handles.find(handle => handle.name === handleId);
|
||||
return found ? found.position : null;
|
||||
};
|
||||
|
||||
// Backward-compatible alias for same-type route crossing validation.
|
||||
const findSameFamilyRouteCrossing = findSameTypeRouteCrossing;
|
||||
|
||||
@@ -1259,6 +1292,7 @@ ${linksYaml}`;
|
||||
createComponentSymbolMetrics,
|
||||
transformPortInfo,
|
||||
getNodePortCanvasPoint,
|
||||
getRotatableNodeHandleDirection,
|
||||
buildPortHandles,
|
||||
buildElementPorts,
|
||||
buildElementPinEntries,
|
||||
|
||||
+157
-72
@@ -1566,6 +1566,7 @@ Organization : OptiHK Limited
|
||||
calculateLayoutBounds,
|
||||
calculateCompositeBoxSize,
|
||||
buildPortHandles,
|
||||
getRotatableNodeHandleDirection,
|
||||
buildElementPorts,
|
||||
getElementPinName,
|
||||
buildElementBoxSize,
|
||||
@@ -1622,7 +1623,7 @@ Organization : OptiHK Limited
|
||||
|
||||
|
||||
// Displays a category icon with cached loading and graceful failure behavior.
|
||||
const IconImg = memo(({ category, containerStyle }) => {
|
||||
const IconImg = memo(({ category, containerStyle, objectFit: imgObjectFit }) => {
|
||||
const [src, setSrc] = useState(() => {
|
||||
if (!category) return undefined;
|
||||
const cache = fetchIcon(category);
|
||||
@@ -1671,7 +1672,7 @@ Organization : OptiHK Limited
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'fill',
|
||||
objectFit: imgObjectFit || 'fill',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
onError={(e) => {
|
||||
@@ -1698,8 +1699,10 @@ Organization : OptiHK Limited
|
||||
useEffect(() => {
|
||||
const transformKey = `${data.rotation || 0}:${data.flip ? 1 : 0}:${data.flop ? 1 : 0}`;
|
||||
if (prevTransformRef.current !== transformKey) {
|
||||
updateNodeInternalsRef.current(id);
|
||||
prevTransformRef.current = transformKey;
|
||||
requestAnimationFrame(() => {
|
||||
updateNodeInternalsRef.current(id);
|
||||
});
|
||||
}
|
||||
}, [data.rotation, data.flip, data.flop, id]);
|
||||
|
||||
@@ -1719,9 +1722,51 @@ Organization : OptiHK Limited
|
||||
top: Position.Top,
|
||||
bottom: Position.Bottom
|
||||
};
|
||||
const rotateHandleDirection = (dir, rot) => {
|
||||
const norm = ((rot % 360) + 360) % 360;
|
||||
const map = {
|
||||
0: { right: 'right', left: 'left', top: 'top', bottom: 'bottom' },
|
||||
90: { right: 'bottom', left: 'top', top: 'left', bottom: 'right' },
|
||||
180: { right: 'left', left: 'right', top: 'bottom', bottom: 'top' },
|
||||
270: { right: 'top', left: 'bottom', top: 'right', bottom: 'left' }
|
||||
};
|
||||
return (map[norm] || map[0])[dir] || dir;
|
||||
};
|
||||
const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||
const flippedPorts = useMemo(
|
||||
() => {
|
||||
const result = {};
|
||||
const ports = Object.entries(data.ports || {}).filter(([name]) => name !== 'a0' && name !== 'b0');
|
||||
if (ports.length === 0) return result;
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
ports.forEach(([, info]) => {
|
||||
const x = Number(info.x || 0);
|
||||
const y = Number(info.y || 0);
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
});
|
||||
ports.forEach(([name, info]) => {
|
||||
let x = Number(info.x || 0);
|
||||
let y = Number(info.y || 0);
|
||||
let a = Number(info.a || 0);
|
||||
if (data.flip) {
|
||||
y = minY + maxY - y;
|
||||
a = -a;
|
||||
}
|
||||
if (data.flop) {
|
||||
x = minX + maxX - x;
|
||||
a = normalizeAngle(180 - a);
|
||||
}
|
||||
result[name] = { ...info, x, y, a: normalizeAngle(a) };
|
||||
});
|
||||
return result;
|
||||
},
|
||||
[data.ports, data.flip, data.flop]
|
||||
);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }),
|
||||
() => buildPortHandles(flippedPorts, { rotation: 0, boxSize: componentSize }),
|
||||
[data.ports, data.rotation, data.flip, data.flop, componentSize]
|
||||
);
|
||||
const portDirectionMap = useMemo(
|
||||
@@ -1731,20 +1776,22 @@ Organization : OptiHK Limited
|
||||
const isAnchorElement = data.elementType === 'anchor';
|
||||
const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName);
|
||||
const visualSize = isAnchorElement ? { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE } : componentSize;
|
||||
const componentVisualTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
|
||||
const componentVisualTransform = `rotate(${data.rotation || 0}deg)`;
|
||||
const componentBodyTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
|
||||
const iconSize = createComponentSymbolMetrics(componentSize);
|
||||
const portLabelStyle = (portHandle) => {
|
||||
const base = { ...portHandle.style };
|
||||
const unrotate = `rotate(${-(data.rotation || 0)}deg)`;
|
||||
if (portHandle.position === 'left') {
|
||||
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
|
||||
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: `${unrotate} translateY(-50%)`, textAlign: 'right' };
|
||||
}
|
||||
if (portHandle.position === 'right') {
|
||||
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: 'translateY(-50%)', textAlign: 'left' };
|
||||
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: `${unrotate} translateY(-50%)`, textAlign: 'left' };
|
||||
}
|
||||
if (portHandle.position === 'top') {
|
||||
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: `${unrotate} translateX(-50%)`, textAlign: 'center' };
|
||||
}
|
||||
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: `${unrotate} translateX(-50%)`, textAlign: 'center' };
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1764,9 +1811,12 @@ Organization : OptiHK Limited
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
minHeight: visualSize.height,
|
||||
overflow: 'hidden',
|
||||
...(visualSize.height < 50 && !isAnchorElement ? { padding: '2px 4px' } : {}),
|
||||
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
|
||||
transform: componentVisualTransform,
|
||||
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
|
||||
transform: componentBodyTransform,
|
||||
transformOrigin: 'center center',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
@@ -1789,8 +1839,8 @@ Organization : OptiHK Limited
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', minHeight: '100%' }}>
|
||||
{!data.hideIcon && data.category && (
|
||||
<div style={{ width: iconSize.width, height: iconSize.height }}>
|
||||
<IconImg category={data.category} />
|
||||
<div style={{ maxWidth: iconSize.width, maxHeight: iconSize.height, width: '100%', aspectRatio: `${iconSize.width}/${iconSize.height}`, overflow: 'hidden' }}>
|
||||
<IconImg category={data.category} objectFit="contain" />
|
||||
</div>
|
||||
)}
|
||||
{!data.category && <div style={{ width: iconSize.width, height: iconSize.height, borderRadius: 4, border: '1px solid var(--border-strong)', background: 'rgba(148, 163, 184, 0.08)' }} />}
|
||||
@@ -1799,39 +1849,43 @@ Organization : OptiHK Limited
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
position: 'absolute',
|
||||
top: 0, left: 0,
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
transform: componentVisualTransform,
|
||||
transformOrigin: 'center center',
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
{portHandles.map((portHandle) => {
|
||||
const originalDir = portDirectionMap.get(portHandle.name) || portHandle.position;
|
||||
const effectiveDir = rotateHandleDirection(originalDir, data.rotation || 0);
|
||||
return (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
position={handlePositionMap[effectiveDir]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
position={handlePositionMap[effectiveDir]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
);
|
||||
})}
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={`label-${portHandle.name}`}>
|
||||
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
||||
<span key={`label-${portHandle.name}`} className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
||||
{portHandle.name}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
return (
|
||||
@@ -1985,29 +2039,22 @@ Organization : OptiHK Limited
|
||||
const name = String(portName || '');
|
||||
return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right';
|
||||
};
|
||||
const anchorHandleVisualStyle = (portHandle, zIndex) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
const anchorHandleVisualStyle = (portHandle, zIndex) => ({
|
||||
...baseHandleStyle,
|
||||
zIndex,
|
||||
left: localLeft,
|
||||
top: localTop,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
};
|
||||
left: portHandle.style?.left,
|
||||
top: portHandle.style?.top || '50%',
|
||||
right: portHandle.style?.right || 'auto',
|
||||
bottom: portHandle.style?.bottom || 'auto',
|
||||
transform: portHandle.style?.transform || 'translate(-50%, -50%)'
|
||||
});
|
||||
const pinLabelStyle = (portHandle) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
left: localLeft,
|
||||
top: localTop,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
left: portHandle.style?.left,
|
||||
top: portHandle.style?.top || '50%',
|
||||
right: portHandle.style?.right || 'auto',
|
||||
bottom: portHandle.style?.bottom || 'auto',
|
||||
transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)'
|
||||
};
|
||||
};
|
||||
@@ -3359,6 +3406,7 @@ Organization : OptiHK Limited
|
||||
const forge = isForgeComponent(componentName);
|
||||
onUpdateNode(selectedNode.id, {
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
componentName,
|
||||
label: componentName,
|
||||
ports: forge ? {} : undefined,
|
||||
@@ -3760,8 +3808,6 @@ Organization : OptiHK Limited
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const canvasViewportRef = useRef(null);
|
||||
const buildLayoutRequestRef = useRef(0);
|
||||
const buildLayoutBusyRef = useRef(false);
|
||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||
|
||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||
@@ -3807,6 +3853,14 @@ Organization : OptiHK Limited
|
||||
}
|
||||
: null
|
||||
), [mouseCanvasPoint, canvasOrigin]);
|
||||
const handleCanvasViewportMoveEnd = useCallback((event, viewport) => {
|
||||
if (!activePageId || !viewport) return;
|
||||
setPages(prev => prev.map(page => (
|
||||
page.id === activePageId
|
||||
? { ...page, viewport: { x: viewport.x, y: viewport.y, zoom: viewport.zoom } }
|
||||
: page
|
||||
)));
|
||||
}, [activePageId]);
|
||||
// Normalizes free-route control points and removes adjacent duplicates before storage.
|
||||
const compactRoutePoints = useCallback((points) => {
|
||||
return (points || [])
|
||||
@@ -3970,8 +4024,10 @@ Organization : OptiHK Limited
|
||||
const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`;
|
||||
const key = [sourceEndpoint, targetEndpoint].sort().join('<>');
|
||||
const group = groups.get(key) || [];
|
||||
const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle);
|
||||
const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle);
|
||||
const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle)
|
||||
|| getRotatableNodeHandleDirection(nodeMap[edge.source], edge.sourceHandle);
|
||||
const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle)
|
||||
|| getRotatableNodeHandleDirection(nodeMap[edge.target], edge.targetHandle);
|
||||
const usesAnchorDirection = Boolean(sourceDirection || targetDirection);
|
||||
const hasRoutePoints = edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2;
|
||||
const directionalEdge = usesAnchorDirection
|
||||
@@ -3994,7 +4050,7 @@ Organization : OptiHK Limited
|
||||
};
|
||||
});
|
||||
return [...separatedEdges, ...rulerEdges];
|
||||
}, [currentEdges, currentNodes, getAnchorHandleRouteDirection, rulerEdges]);
|
||||
}, [currentEdges, currentNodes, getAnchorHandleRouteDirection, getRotatableNodeHandleDirection, rulerEdges]);
|
||||
|
||||
const [projectCompositeMap, setProjectCompositeMap] = useState({});
|
||||
const [standaloneComposites, setStandaloneComposites] = useState([]);
|
||||
@@ -5058,6 +5114,35 @@ Organization : OptiHK Limited
|
||||
return boxSize ? { ...node, data: { ...node.data, boxSize } } : node;
|
||||
})
|
||||
}));
|
||||
|
||||
// Pre-fetch PDK component metadata so nodes render with correct boxSize immediately.
|
||||
const allNodes = cellPages.flatMap(page => page.nodes);
|
||||
const pdkNames = [...new Set(allNodes
|
||||
.filter(n => n.data?.componentName && !n.data?.elementType
|
||||
&& !isForgeComponent(n.data.componentName)
|
||||
&& !isBasicComponent(n.data.componentName))
|
||||
.map(n => n.data.componentName))];
|
||||
if (pdkNames.length > 0) {
|
||||
const metaResults = await Promise.all(
|
||||
pdkNames.map(name => loadComponentMetadata(name).catch(() => null))
|
||||
);
|
||||
const metaMap = new Map(
|
||||
pdkNames.filter((_, i) => metaResults[i]).map((name, i) => [name, metaResults[i]])
|
||||
);
|
||||
for (const page of cellPages) {
|
||||
page.nodes = page.nodes.map(node => {
|
||||
const metadata = metaMap.get(node.data?.componentName);
|
||||
if (!metadata) return node;
|
||||
const sz = normalizeBoxSize(metadata);
|
||||
return {
|
||||
...node,
|
||||
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz),
|
||||
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '' }
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
|
||||
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
||||
const resolvedProjectPage = loadedProjectPage || projectPage;
|
||||
@@ -5084,12 +5169,18 @@ Organization : OptiHK Limited
|
||||
|
||||
useEffect(() => {
|
||||
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
||||
if (activePage.viewport) {
|
||||
window.requestAnimationFrame(() => {
|
||||
reactFlowInstance.setViewport(activePage.viewport, { duration: 0 });
|
||||
});
|
||||
} else {
|
||||
reactFlowInstance.fitBounds(
|
||||
{ x: 0, y: 0, width: activeCanvasSize.width, height: activeCanvasSize.height },
|
||||
{ padding: 0.12, duration: 0 }
|
||||
);
|
||||
}
|
||||
}, [activePage?.id, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]);
|
||||
}
|
||||
}, [activePage?.id, activePage?.viewport, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]);
|
||||
|
||||
useEffect(() => {
|
||||
setRulerStartPoint(null);
|
||||
@@ -5176,12 +5267,20 @@ Organization : OptiHK Limited
|
||||
};
|
||||
})
|
||||
})));
|
||||
|
||||
// Force React Flow to re-measure nodes whose boxSize / ports have changed.
|
||||
requestAnimationFrame(() => {
|
||||
const updatedIds = results.filter(r => r.metadata).map(r => r.nodeId);
|
||||
if (updatedIds.length > 0 && reactFlowInstance.updateNodeInternals) {
|
||||
reactFlowInstance.updateNodeInternals(updatedIds);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pages, loadComponentMetadata]);
|
||||
}, [pages, loadComponentMetadata, reactFlowInstance]);
|
||||
|
||||
const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]);
|
||||
|
||||
@@ -5938,6 +6037,7 @@ Organization : OptiHK Limited
|
||||
const route = currentLinkRoute;
|
||||
const view = routeStyleForSettings(route, false);
|
||||
const edgeId = `edge-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}-${Date.now()}`;
|
||||
const nodeMap = Object.fromEntries(activePage.nodes.map(node => [node.id, node]));
|
||||
const candidate = {
|
||||
id: edgeId,
|
||||
source: connection.source,
|
||||
@@ -5947,9 +6047,8 @@ Organization : OptiHK Limited
|
||||
type: view.type,
|
||||
selectable: true,
|
||||
style: view.style,
|
||||
data: { route }
|
||||
data: { route },
|
||||
};
|
||||
const nodeMap = Object.fromEntries(activePage.nodes.map(node => [node.id, node]));
|
||||
const conflict = findSameTypeRouteCrossing(candidate, activePage.edges, nodeMap, technologyManifest);
|
||||
if (conflict) {
|
||||
const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source;
|
||||
@@ -5963,7 +6062,7 @@ Organization : OptiHK Limited
|
||||
: p
|
||||
)));
|
||||
addLog(`Connected ${connection.sourceHandle} to ${connection.targetHandle}.`);
|
||||
}, [activePageId, activePage, rulerMode, currentLinkRoute, technologyManifest, addLog]);
|
||||
}, [activePageId, activePage, rulerMode, currentLinkRoute, technologyManifest, addLog, getAnchorHandleRouteDirection]);
|
||||
|
||||
// Select custom route edges from their SVG hit target.
|
||||
const handleRouteEdgeMouseDown = useCallback((event) => {
|
||||
@@ -6210,64 +6309,49 @@ ${bundlesBlock}`;
|
||||
// Save the active page, generate layout preview assets, and show the preview tab.
|
||||
const handleBuildLayout = useCallback(async () => {
|
||||
if (!activePage) return;
|
||||
if (buildLayoutBusyRef.current) return;
|
||||
if (buildLayoutBusy) return;
|
||||
if (!validateRouteCrossings(activePage)) return;
|
||||
const buildPage = activePage;
|
||||
const buildRequestId = buildLayoutRequestRef.current + 1;
|
||||
buildLayoutRequestRef.current = buildRequestId;
|
||||
buildLayoutBusyRef.current = true;
|
||||
setBuildLayoutBusy(true);
|
||||
startBuildProgress('Building layout');
|
||||
const yamlContent = buildYamlForPage(buildPage);
|
||||
const layoutBounds = calculateLayoutBounds(buildPage);
|
||||
const yamlContent = buildYamlForPage(activePage);
|
||||
|
||||
// send to backend
|
||||
try {
|
||||
const response = await fetch('/api/save-layout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
project: currentProjectName,
|
||||
cell: buildPage.name,
|
||||
cell: activePage.name,
|
||||
content: yamlContent,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
const errData = await response.json();
|
||||
addLog(errData.error || 'Save failed, unknown error');
|
||||
stopBuildProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Successfully saved: ' + result.path);
|
||||
if (result.preview_error) {
|
||||
addLog('Preview skipped: ' + result.preview_error);
|
||||
}
|
||||
if (result.svg_ready && result.svg_url) {
|
||||
if (result.svg_url) {
|
||||
completeBuildProgress('Layout ready');
|
||||
openLayoutPreview(buildPage.name, result.svg_url, layoutBounds);
|
||||
openLayoutPreview(activePage.name, result.svg_url, calculateLayoutBounds(activePage));
|
||||
} else {
|
||||
if (result.preview_status === 'generated') {
|
||||
addLog('Layout SVG was not marked ready by the backend.');
|
||||
}
|
||||
completeBuildProgress('Layout saved');
|
||||
}
|
||||
} catch (err) {
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Save error: ' + err.message);
|
||||
stopBuildProgress();
|
||||
} finally {
|
||||
if (buildRequestId === buildLayoutRequestRef.current) {
|
||||
buildLayoutBusyRef.current = false;
|
||||
setBuildLayoutBusy(false);
|
||||
}
|
||||
}
|
||||
}, [activePage, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
||||
}, [activePage, buildLayoutBusy, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
||||
|
||||
// Save YAML for every editable project/composite page without opening previews.
|
||||
const handleSaveProjectLayouts = useCallback(async () => {
|
||||
@@ -6551,6 +6635,7 @@ ${bundlesBlock}`;
|
||||
minZoom={0.02}
|
||||
maxZoom={4}
|
||||
defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}
|
||||
onMoveEnd={handleCanvasViewportMoveEnd}
|
||||
panOnDrag={false}
|
||||
selectionOnDrag={true}
|
||||
selectionMode={FULL_SELECTION_MODE}
|
||||
|
||||
@@ -0,0 +1,829 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% raw %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>mxPIC Core - Canvas</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/reactflow@11/dist/umd/index.js" crossorigin></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/reactflow@11/dist/style.css" />
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
/* optihk Shared Dark Theme Variables */
|
||||
:root {
|
||||
--bg-main: #0f172a;
|
||||
--bg-card: #1e293b;
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--accent: #38bdf8;
|
||||
--accent-hover: #0284c7;
|
||||
--border: #334155;
|
||||
--input-bg: #0b1120;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-main);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Custom Dark Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-main);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Tree View Styling */
|
||||
details {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.tree-folder summary {
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.component-leaf {
|
||||
cursor: grab;
|
||||
padding: 4px 6px;
|
||||
margin-left: 15px;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.component-leaf:hover {
|
||||
background: var(--border);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Side Panel Blocks */
|
||||
.left-block, .right-block {
|
||||
background: var(--bg-main);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-block-header, .right-block-header {
|
||||
background: var(--bg-card);
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.left-block-body, .right-block-body {
|
||||
padding: 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.placeholder-block {
|
||||
border: 1px dashed var(--border);
|
||||
padding: 12px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
background: var(--bg-main);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2em;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: var(--border);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Standard Form Inputs inside panels */
|
||||
input[type="number"], input[type="text"] {
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
input[type="number"]:focus, input[type="text"]:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ReactFlow Dark Mode Overrides */
|
||||
.react-flow__controls button {
|
||||
background-color: var(--bg-card) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
fill: var(--text-main) !important;
|
||||
}
|
||||
.react-flow__controls button:hover {
|
||||
background-color: var(--border) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect, useRef, useCallback, useMemo, memo } = React;
|
||||
const {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
Background,
|
||||
useReactFlow,
|
||||
addEdge,
|
||||
Handle,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
} = window.ReactFlow;
|
||||
|
||||
// --- NODE DESIGN (Dark CAD Style) ---
|
||||
const RotatableNode = ({ id, data, selected }) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [data.rotation, updateNodeInternals, id]);
|
||||
|
||||
const baseHandleStyle = {
|
||||
width: 10, height: 10,
|
||||
background: 'var(--bg-main)',
|
||||
border: '2px solid var(--accent)',
|
||||
borderRadius: '50%',
|
||||
};
|
||||
const leftTopPort = { ...baseHandleStyle, top: '24%', transform: 'translate(-50%, -50%)' };
|
||||
const leftBottomPort = { ...baseHandleStyle, top: '76%', transform: 'translate(-50%, -50%)' };
|
||||
const rightTopPort = { ...baseHandleStyle, top: '24%', transform: 'translate(50%, -50%)' };
|
||||
const rightBottomPort = { ...baseHandleStyle, top: '76%', transform: 'translate(50%, -50%)' };
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px 20px',
|
||||
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
background: 'var(--bg-card)',
|
||||
color: 'var(--text-main)',
|
||||
minWidth: 100, textAlign: 'center',
|
||||
position: 'relative', transform: `rotate(${data.rotation || 0}deg)`,
|
||||
transition: selected ? 'none' : 'transform 0.1s ease',
|
||||
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",
|
||||
fontSize: '0.85rem'
|
||||
}}>
|
||||
<div>{data.componentDisplayName}</div>
|
||||
<Handle type="source" position={Position.Left} id="port-lt-source" style={{ ...leftTopPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Left} id="port-lt-target" style={{ ...leftTopPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Left} id="port-lb-source" style={{ ...leftBottomPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Left} id="port-lb-target" style={{ ...leftBottomPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Right} id="port-rt-source" style={{ ...rightTopPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Right} id="port-rt-target" style={{ ...rightTopPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Right} id="port-rb-source" style={{ ...rightBottomPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Right} id="port-rb-target" style={{ ...rightBottomPort, zIndex: 5 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeNode = ({ name, children }) => {
|
||||
if (children && children.__type__ === 'component') {
|
||||
const componentName = children.__name__;
|
||||
const handleDragStart = (event) => {
|
||||
event.dataTransfer.setData('application/reactflow', componentName);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
return (
|
||||
<div className="component-leaf" draggable onDragStart={handleDragStart}>
|
||||
<span style={{color: 'var(--accent)', marginRight: '4px'}}>❖</span> {name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasChildren = children && Object.keys(children).length > 0;
|
||||
return (
|
||||
<details>
|
||||
<summary className="tree-folder">
|
||||
<span style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>📂 {name}</span>
|
||||
</summary>
|
||||
{hasChildren &&
|
||||
Object.entries(children).map(([childName, childData]) => (
|
||||
<TreeNode key={childName} name={childName} children={childData} />
|
||||
))
|
||||
}
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
const LeftPanel = ({ library, treeKey, expanded, onToggle, treeRef, width }) => (
|
||||
<aside style={{
|
||||
width: width, background: 'var(--bg-card)', borderRight: '1px solid var(--border)',
|
||||
padding: 12, display: 'flex', flexDirection: 'column', height: '100%',
|
||||
boxSizing: 'border-box', overflowY: 'auto'
|
||||
}}>
|
||||
<div className="left-block">
|
||||
<div className="left-block-header">
|
||||
<span>PDK Libraries</span>
|
||||
<button className="toggle-btn" onClick={onToggle} title={expanded ? 'Collapse all' : 'Expand all'}>
|
||||
{expanded ? '▾' : '▸'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="left-block-body" style={{ maxHeight: '45vh', overflowY: 'auto' }} key={treeKey} ref={treeRef}>
|
||||
{library && Object.keys(library).length > 0 ? (
|
||||
Object.entries(library).map(([key, value]) => (
|
||||
<TreeNode key={key} name={key} children={value} />
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>Loading library...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="left-block">
|
||||
<div className="left-block-header">Routing modes</div>
|
||||
<div className="left-block-body">
|
||||
<ul style={{ paddingLeft: 20, margin: 0, color: 'var(--text-muted)', lineHeight: '1.8' }}>
|
||||
<li>Single mode wires</li>
|
||||
<li>Multi-mode wires</li>
|
||||
<li>DC electrical wires</li>
|
||||
<li>RF electrical wires</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="left-block" style={{ marginTop: 'auto' }}>
|
||||
<div className="left-block-header">Session</div>
|
||||
<div className="left-block-body" style={{color: 'var(--text-muted)'}}>
|
||||
<div style={{marginBottom: '4px'}}>Name: XXXXXX</div>
|
||||
<div style={{marginBottom: '10px'}}>ID: 12345678</div>
|
||||
<button disabled style={{
|
||||
background: 'var(--border)', color: 'var(--text-muted)',
|
||||
border: 'none', padding: '6px 12px', borderRadius: '4px', width: '100%'
|
||||
}}>Log out</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const RightPanel = memo(({ selectedNode, width, onRenameComponent }) => {
|
||||
const [componentData, setComponentData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [enlarged, setEnlarged] = useState(null);
|
||||
const { setNodes } = useReactFlow();
|
||||
const [editingComponentName, setEditingComponentName] = useState(false);
|
||||
const [tempComponentName, setTempComponentName] = useState('');
|
||||
const [localX, setLocalX] = useState('');
|
||||
const [localY, setLocalY] = useState('');
|
||||
const [localRotation, setLocalRotation] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const nodeId = selectedNode?.id;
|
||||
if (!nodeId) {
|
||||
setComponentData(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const compName = selectedNode?.data?.componentName;
|
||||
if (!compName) {
|
||||
setComponentData(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (componentData && componentData.name === compName && componentData.nodeId === nodeId) return;
|
||||
|
||||
setLoading(true);
|
||||
fetch(`/api/component/${encodeURIComponent(compName)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setLocalX(selectedNode.position.x.toFixed(3));
|
||||
setLocalY(selectedNode.position.y.toFixed(3));
|
||||
setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3));
|
||||
}
|
||||
}, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.id]);
|
||||
|
||||
const updatePosition = useCallback((id, axis, value) => {
|
||||
const val = parseFloat(value);
|
||||
if (isNaN(val)) return;
|
||||
setNodes(nds => nds.map(n => n.id === id ? { ...n, position: { ...n.position, [axis]: val } } : n));
|
||||
}, [setNodes]);
|
||||
|
||||
const updateRotation = useCallback((id, value) => {
|
||||
const val = parseFloat(value);
|
||||
if (isNaN(val)) return;
|
||||
const clamped = Math.min(180, Math.max(-180, val));
|
||||
setNodes(nds => nds.map(n => n.id === id ? { ...n, data: { ...n.data, rotation: clamped } } : n));
|
||||
}, [setNodes]);
|
||||
|
||||
const formatPort = (port) => {
|
||||
if (!port) return '—';
|
||||
return `x:${port.x ?? '?'}, y:${port.y ?? '?'}, a:${port.a ?? '?'}, w:${port.width ?? '?'}`;
|
||||
};
|
||||
|
||||
const currentRotation = selectedNode?.data?.rotation ?? 0;
|
||||
const currentComponentDisplayName = selectedNode?.data?.componentDisplayName || '';
|
||||
|
||||
const handleStartEditName = () => {
|
||||
setTempComponentName(currentComponentDisplayName);
|
||||
setEditingComponentName(true);
|
||||
};
|
||||
|
||||
const handleSaveName = () => {
|
||||
const newName = tempComponentName.trim();
|
||||
if (newName && selectedNode) {
|
||||
onRenameComponent(selectedNode.id, newName);
|
||||
}
|
||||
setEditingComponentName(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveName();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingComponentName(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside style={{
|
||||
width: width, background: 'var(--bg-card)', borderLeft: '1px solid var(--border)',
|
||||
padding: 12, display: 'flex', flexDirection: 'column', height: '100%',
|
||||
boxSizing: 'border-box', overflowY: 'auto'
|
||||
}}>
|
||||
<div className="right-block">
|
||||
<div className="right-block-header">Transforms</div>
|
||||
<div className="right-block-body">
|
||||
{selectedNode ? (
|
||||
<div>
|
||||
<label>X Coordinate</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localX}
|
||||
onChange={(e) => setLocalX(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localX);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updatePosition(selectedNode.id, 'x', val);
|
||||
setLocalX(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalX(selectedNode.position.x.toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') e.currentTarget.blur();
|
||||
}}
|
||||
/>
|
||||
<br /><br />
|
||||
<label>Y Coordinate</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localY}
|
||||
onChange={(e) => setLocalY(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localY);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updatePosition(selectedNode.id, 'y', val);
|
||||
setLocalY(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalY(selectedNode.position.y.toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') e.currentTarget.blur();
|
||||
}}
|
||||
/>
|
||||
<br /><br />
|
||||
<label>Angle (deg)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localRotation}
|
||||
onChange={(e) => setLocalRotation(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localRotation);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updateRotation(selectedNode.id, val);
|
||||
setLocalRotation(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') e.currentTarget.blur();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-muted)', fontStyle: 'italic', textAlign: 'center' }}>Select a node to inspect</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedNode?.data?.componentName && (
|
||||
<div className="right-block">
|
||||
<div className="right-block-header">Parameters</div>
|
||||
<div className="right-block-body">
|
||||
{loading ? (
|
||||
<p style={{color: 'var(--text-muted)'}}>Loading data...</p>
|
||||
) : componentData ? (
|
||||
<>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label>Instance Name</label>
|
||||
{editingComponentName ? (
|
||||
<input
|
||||
type="text"
|
||||
value={tempComponentName}
|
||||
onChange={(e) => setTempComponentName(e.target.value)}
|
||||
onBlur={handleSaveName}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'var(--input-bg)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
color: 'var(--accent)'
|
||||
}}
|
||||
onClick={handleStartEditName}
|
||||
title="Click to edit"
|
||||
>
|
||||
<span>{currentComponentDisplayName || componentData.name}</span>
|
||||
<span style={{fontSize: '12px', color: 'var(--text-muted)'}}>✎</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{color: 'var(--text-muted)', lineHeight: '1.6'}}>
|
||||
<p style={{ margin: '0 0 8px 0', wordBreak: 'break-all' }}>
|
||||
<strong style={{color: 'var(--text-main)'}}>Cell:</strong> {componentData.name}
|
||||
</p>
|
||||
<p style={{ margin: '0 0 8px 0' }}>
|
||||
<strong style={{color: 'var(--text-main)'}}>Foundry:</strong> {componentData.foundry}<br/>
|
||||
<strong style={{color: 'var(--text-main)'}}>Process:</strong> {componentData.process}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style={{color: 'var(--text-main)', fontWeight: '500', marginBottom: '4px'}}>Ports:</p>
|
||||
<ul style={{ paddingLeft: 15, margin: '0 0 15px 0', color: 'var(--text-muted)' }}>
|
||||
{componentData.ports && Object.entries(componentData.ports).map(([portName, portInfo]) => (
|
||||
<li key={portName} style={{ letterSpacing: '0.5px' }}>
|
||||
<span style={{color: 'var(--accent)'}}>{portName}</span>: {formatPort(portInfo)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p style={{color: 'var(--text-main)', fontWeight: '500', marginBottom: '4px'}}>Preview:</p>
|
||||
<div style={{
|
||||
border: '1px solid var(--border)', width: '100%', height: 100,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'var(--input-bg)', borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<img
|
||||
src={`/api/component/${encodeURIComponent(componentData.name)}/image`}
|
||||
alt="Component layout"
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', cursor: 'pointer' }}
|
||||
onClick={() => setEnlarged(`/api/component/${encodeURIComponent(componentData.name)}/image`)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.parentElement.innerHTML = '<span style="color:var(--text-muted)">No preview</span>';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-muted)' }}>No data available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="right-block" style={{ marginTop: 'auto' }}>
|
||||
<div className="right-block-header">Inverse Design</div>
|
||||
<div className="right-block-body placeholder-block">Requires AI Upgrade</div>
|
||||
</div>
|
||||
|
||||
{enlarged && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.9)', zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'zoom-out',
|
||||
backdropFilter: 'blur(4px)'
|
||||
}}
|
||||
onClick={() => setEnlarged(null)}
|
||||
>
|
||||
<img
|
||||
src={enlarged}
|
||||
alt="Enlarged layout"
|
||||
style={{ maxWidth: '90%', maxHeight: '90%', objectFit: 'contain', border: '1px solid var(--border)', background: 'var(--bg-main)' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
const prev = prevProps.selectedNode;
|
||||
const next = nextProps.selectedNode;
|
||||
if (prev?.id !== next?.id) return false;
|
||||
if (prev?.position?.x !== next?.position?.x) return false;
|
||||
if (prev?.position?.y !== next?.position?.y) return false;
|
||||
if (prev?.data?.rotation !== next?.data?.rotation) return false;
|
||||
if (prev?.data?.componentName !== next?.data?.componentName) return false;
|
||||
if (prev?.data?.componentDisplayName !== next?.data?.componentDisplayName) return false;
|
||||
if (prevProps.width !== nextProps.width) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const ResizeHandle = ({ onMouseDown }) => (
|
||||
<div
|
||||
onMouseDown={onMouseDown}
|
||||
style={{
|
||||
width: 6, cursor: 'col-resize', background: 'transparent',
|
||||
transition: 'background 0.2s', zIndex: 5, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--accent)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||
/>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
|
||||
const [library, setLibrary] = useState(null);
|
||||
const [treeKey, setTreeKey] = useState(0);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const treeContainerRef = useRef(null);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(260);
|
||||
const [rightWidth, setRightWidth] = useState(260);
|
||||
const [dragging, setDragging] = useState(null);
|
||||
|
||||
const [gridSnap, setGridSnap] = useState(false);
|
||||
|
||||
const componentCounterRef = useRef(1);
|
||||
|
||||
const generateComponentDisplayName = useCallback(() => {
|
||||
const name = `component_${componentCounterRef.current}`;
|
||||
componentCounterRef.current += 1;
|
||||
return name;
|
||||
}, []);
|
||||
|
||||
const renameComponent = useCallback((nodeId, newComponentDisplayName) => {
|
||||
setNodes(nds => nds.map(n => {
|
||||
if (n.id === nodeId) {
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
componentDisplayName: newComponentDisplayName
|
||||
}
|
||||
};
|
||||
}
|
||||
return n;
|
||||
}));
|
||||
}, [setNodes]);
|
||||
|
||||
const fetchLibrary = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/library');
|
||||
const data = await res.json();
|
||||
setLibrary(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch library', err);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => { fetchLibrary(); }, [fetchLibrary]);
|
||||
|
||||
const selectedNode = useMemo(() => nodes.find(n => n.selected), [nodes]);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
if (!type) return;
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const componentDisplayName = generateComponentDisplayName();
|
||||
const newNode = {
|
||||
id: Date.now().toString(),
|
||||
type: 'rotatableNode',
|
||||
position,
|
||||
data: {
|
||||
label: type,
|
||||
componentName: type,
|
||||
rotation: 0,
|
||||
componentDisplayName: componentDisplayName
|
||||
},
|
||||
};
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [setNodes, reactFlowInstance, generateComponentDisplayName]);
|
||||
|
||||
const onConnect = useCallback((connection) => {
|
||||
setEdges((eds) => addEdge({ ...connection, type: 'smoothstep', style: { stroke: 'var(--accent)', strokeWidth: 2 } }, eds));
|
||||
}, [setEdges]);
|
||||
|
||||
const expandAll = useCallback(() => {
|
||||
if (treeContainerRef.current) {
|
||||
treeContainerRef.current.querySelectorAll('details').forEach(d => d.open = true);
|
||||
}
|
||||
}, []);
|
||||
const collapseAll = useCallback(() => setTreeKey(k => k + 1), []);
|
||||
const handleToggle = useCallback(() => {
|
||||
if (expanded) { collapseAll(); setExpanded(false); }
|
||||
else { expandAll(); setExpanded(true); }
|
||||
}, [expanded, expandAll, collapseAll]);
|
||||
|
||||
const handleResizeStart = useCallback((side) => (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(side);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const onMouseMove = (e) => {
|
||||
if (dragging === 'left') {
|
||||
setLeftWidth(Math.min(500, Math.max(150, e.clientX)));
|
||||
} else if (dragging === 'right') {
|
||||
const newWidth = window.innerWidth - e.clientX;
|
||||
setRightWidth(Math.min(500, Math.max(150, newWidth)));
|
||||
}
|
||||
};
|
||||
const onMouseUp = () => setDragging(null);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}, [dragging]);
|
||||
|
||||
const toggleGridSnap = useCallback(() => {
|
||||
setGridSnap(prev => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%', userSelect: dragging ? 'none' : 'auto' }}>
|
||||
<LeftPanel
|
||||
library={library} treeKey={treeKey} expanded={expanded}
|
||||
onToggle={handleToggle} treeRef={treeContainerRef} width={leftWidth}
|
||||
/>
|
||||
<ResizeHandle onMouseDown={handleResizeStart('left')} />
|
||||
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
|
||||
{/* Grid Snap Toggle Switch */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 15, right: 15, zIndex: 10,
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
background: 'var(--bg-card)', padding: '6px 12px', borderRadius: '8px',
|
||||
border: '1px solid var(--border)', boxShadow: '0 4px 6px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: '0.85em', fontWeight: '500', color: 'var(--text-main)', userSelect: 'none'
|
||||
}}>Snap to Grid</span>
|
||||
<div
|
||||
onClick={toggleGridSnap}
|
||||
style={{
|
||||
width: 40, height: 20, borderRadius: 10,
|
||||
background: gridSnap ? 'var(--accent)' : 'var(--input-bg)',
|
||||
border: '1px solid ' + (gridSnap ? 'var(--accent)' : 'var(--border)'),
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center',
|
||||
padding: '0 2px', transition: 'background 0.3s, border-color 0.3s',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 16, height: 16, borderRadius: '50%',
|
||||
background: '#fff',
|
||||
transform: gridSnap ? 'translateX(20px)' : 'translateX(0)',
|
||||
transition: 'transform 0.2s',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={{ rotatableNode: RotatableNode }}
|
||||
fitView
|
||||
snapToGrid={gridSnap}
|
||||
snapGrid={[10, 10]}
|
||||
nodesDraggable={true}
|
||||
nodesConnectable={true}
|
||||
elementsSelectable={true}
|
||||
connectionRadius={50}
|
||||
>
|
||||
<Controls style={{ bottom: 15, left: 15 }} />
|
||||
{/* Dark mode background for the canvas */}
|
||||
<Background color="#334155" gap={20} size={1} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
<ResizeHandle onMouseDown={handleResizeStart('right')} />
|
||||
<RightPanel
|
||||
selectedNode={selectedNode}
|
||||
width={rightWidth}
|
||||
onRenameComponent={renameComponent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<ReactFlowProvider>
|
||||
<App />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
{% endraw %}
|
||||
@@ -0,0 +1,750 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% raw %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Canvas with PDK Library – Component Name & Rotation</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/reactflow@11/dist/umd/index.js" crossorigin></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/reactflow@11/dist/style.css" />
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
body,
|
||||
html,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
details {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-block {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.left-block-header {
|
||||
background: #e0e0e0;
|
||||
padding: 6px 10px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.left-block-body {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.tree-folder summary {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.right-block {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.right-block-header {
|
||||
background: #e0e0e0;
|
||||
padding: 6px 10px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.right-block-body {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.placeholder-block {
|
||||
border: 1px dashed #bbb;
|
||||
padding: 8px;
|
||||
color: #888;
|
||||
font-size: 0.85em;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.8em;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.component-leaf {
|
||||
cursor: grab;
|
||||
padding: 2px 4px;
|
||||
margin-left: 20px;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.component-leaf:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect, useRef, useCallback, useMemo, memo } = React;
|
||||
const {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
Background,
|
||||
useReactFlow,
|
||||
addEdge,
|
||||
Handle,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
} = window.ReactFlow;
|
||||
|
||||
const RotatableNode = ({ id, data, selected }) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [data.rotation, updateNodeInternals, id]);
|
||||
|
||||
const baseHandleStyle = {
|
||||
width: 14, height: 14,
|
||||
background: '#555',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
};
|
||||
const leftTopPort = { ...baseHandleStyle, top: '24%', transform: 'translate(-50%, -50%)' };
|
||||
const leftBottomPort = { ...baseHandleStyle, top: '76%', transform: 'translate(-50%, -50%)' };
|
||||
const rightTopPort = { ...baseHandleStyle, top: '24%', transform: 'translate(50%, -50%)' };
|
||||
const rightBottomPort = { ...baseHandleStyle, top: '76%', transform: 'translate(50%, -50%)' };
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px 20px', border: '1px solid #333', borderRadius: 6,
|
||||
background: '#fff', minWidth: 100, textAlign: 'center',
|
||||
position: 'relative', transform: `rotate(${data.rotation || 0}deg)`,
|
||||
transition: selected ? 'none' : 'transform 0.1s ease',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
<div>{data.componentDisplayName}</div>
|
||||
<Handle type="source" position={Position.Left} id="port-lt-source" style={{ ...leftTopPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Left} id="port-lt-target" style={{ ...leftTopPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Left} id="port-lb-source" style={{ ...leftBottomPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Left} id="port-lb-target" style={{ ...leftBottomPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Right} id="port-rt-source" style={{ ...rightTopPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Right} id="port-rt-target" style={{ ...rightTopPort, zIndex: 5 }} />
|
||||
<Handle type="source" position={Position.Right} id="port-rb-source" style={{ ...rightBottomPort, zIndex: 10 }} />
|
||||
<Handle type="target" position={Position.Right} id="port-rb-target" style={{ ...rightBottomPort, zIndex: 5 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeNode = ({ name, children }) => {
|
||||
if (children && children.__type__ === 'component') {
|
||||
const componentName = children.__name__;
|
||||
const handleDragStart = (event) => {
|
||||
event.dataTransfer.setData('application/reactflow', componentName);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
return (
|
||||
<div className="component-leaf" draggable onDragStart={handleDragStart}>
|
||||
🔷 {name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasChildren = children && Object.keys(children).length > 0;
|
||||
return (
|
||||
<details>
|
||||
<summary className="tree-folder">
|
||||
<span style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>📂 {name}</span>
|
||||
</summary>
|
||||
{hasChildren &&
|
||||
Object.entries(children).map(([childName, childData]) => (
|
||||
<TreeNode key={childName} name={childName} children={childData} />
|
||||
))
|
||||
}
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
const LeftPanel = ({ library, treeKey, expanded, onToggle, treeRef, width }) => (
|
||||
<aside style={{
|
||||
width: width, background: '#f4f4f4', borderRight: '1px solid #ccc',
|
||||
padding: 10, display: 'flex', flexDirection: 'column', height: '100%',
|
||||
boxSizing: 'border-box', overflowY: 'auto'
|
||||
}}>
|
||||
<div className="left-block">
|
||||
<div className="left-block-header">
|
||||
<span>PDK Libraries</span>
|
||||
<button className="toggle-btn" onClick={onToggle} title={expanded ? 'Collapse all' : 'Expand all'}>
|
||||
{expanded ? '▾' : '▸'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="left-block-body" style={{ maxHeight: '45vh', overflowY: 'auto' }} key={treeKey} ref={treeRef}>
|
||||
{library && Object.keys(library).length > 0 ? (
|
||||
Object.entries(library).map(([key, value]) => (
|
||||
<TreeNode key={key} name={key} children={value} />
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#999' }}>Loading library...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="left-block">
|
||||
<div className="left-block-header">Routing selections</div>
|
||||
<div className="left-block-body">
|
||||
<ul style={{ paddingLeft: 20, margin: 0 }}>
|
||||
<li>Single mode wires</li>
|
||||
<li>Multi-mode wires</li>
|
||||
<li>DC electrical wires</li>
|
||||
<li>RF electrical wires</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="left-block" style={{ marginTop: 'auto' }}>
|
||||
<div className="left-block-header">User info</div>
|
||||
<div className="left-block-body">
|
||||
<div>Name: XXXXXX</div>
|
||||
<div>ID: 12345678</div>
|
||||
<button disabled>Log out</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const RightPanel = memo(({ selectedNode, width, onRenameComponent }) => {
|
||||
const [componentData, setComponentData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [enlarged, setEnlarged] = useState(null);
|
||||
const { setNodes } = useReactFlow();
|
||||
const [editingComponentName, setEditingComponentName] = useState(false);
|
||||
const [tempComponentName, setTempComponentName] = useState('');
|
||||
const [localX, setLocalX] = useState('');
|
||||
const [localY, setLocalY] = useState('');
|
||||
const [localRotation, setLocalRotation] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const nodeId = selectedNode?.id;
|
||||
if (!nodeId) {
|
||||
setComponentData(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const compName = selectedNode?.data?.componentName;
|
||||
if (!compName) {
|
||||
setComponentData(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (componentData && componentData.name === compName && componentData.nodeId === nodeId) return;
|
||||
|
||||
setLoading(true);
|
||||
fetch(`/api/component/${encodeURIComponent(compName)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setLocalX(selectedNode.position.x.toFixed(3));
|
||||
setLocalY(selectedNode.position.y.toFixed(3));
|
||||
setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3));
|
||||
}
|
||||
}, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.id]);
|
||||
|
||||
const updatePosition = useCallback((id, axis, value) => {
|
||||
const val = parseFloat(value);
|
||||
if (isNaN(val)) return;
|
||||
setNodes(nds => nds.map(n => n.id === id ? { ...n, position: { ...n.position, [axis]: val } } : n));
|
||||
}, [setNodes]);
|
||||
|
||||
const updateRotation = useCallback((id, value) => {
|
||||
const val = parseFloat(value);
|
||||
if (isNaN(val)) return;
|
||||
const clamped = Math.min(180, Math.max(-180, val));
|
||||
setNodes(nds => nds.map(n => n.id === id ? { ...n, data: { ...n.data, rotation: clamped } } : n));
|
||||
}, [setNodes]);
|
||||
|
||||
const formatPort = (port) => {
|
||||
if (!port) return '—';
|
||||
return `x:${port.x ?? '?'}, y:${port.y ?? '?'}, a:${port.a ?? '?'}, w:${port.width ?? '?'}`;
|
||||
};
|
||||
|
||||
const currentRotation = selectedNode?.data?.rotation ?? 0;
|
||||
const currentComponentDisplayName = selectedNode?.data?.componentDisplayName || '';
|
||||
|
||||
const handleStartEditName = () => {
|
||||
setTempComponentName(currentComponentDisplayName);
|
||||
setEditingComponentName(true);
|
||||
};
|
||||
|
||||
const handleSaveName = () => {
|
||||
const newName = tempComponentName.trim();
|
||||
if (newName && selectedNode) {
|
||||
onRenameComponent(selectedNode.id, newName);
|
||||
}
|
||||
setEditingComponentName(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveName();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingComponentName(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside style={{
|
||||
width: width, background: '#fafafa', borderLeft: '1px solid #ccc',
|
||||
padding: 10, display: 'flex', flexDirection: 'column', height: '100%',
|
||||
boxSizing: 'border-box', overflowY: 'auto'
|
||||
}}>
|
||||
<div className="right-block">
|
||||
<div className="right-block-header">Properties</div>
|
||||
<div className="right-block-body">
|
||||
{selectedNode ? (
|
||||
<div>
|
||||
<label>X:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localX}
|
||||
onChange={(e) => setLocalX(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localX);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updatePosition(selectedNode.id, 'x', val);
|
||||
setLocalX(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalX(selectedNode.position.x.toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<br /><br />
|
||||
<label>Y:</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localY}
|
||||
onChange={(e) => setLocalY(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localY);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updatePosition(selectedNode.id, 'y', val);
|
||||
setLocalY(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalY(selectedNode.position.y.toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<br /><br />
|
||||
<label>A (deg):</label>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
value={localRotation}
|
||||
onChange={(e) => setLocalRotation(e.target.value)}
|
||||
onBlur={() => {
|
||||
const val = parseFloat(localRotation);
|
||||
if (!isNaN(val) && selectedNode) {
|
||||
updateRotation(selectedNode.id, val);
|
||||
setLocalRotation(val.toFixed(3));
|
||||
} else if (selectedNode) {
|
||||
setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ color: '#999' }}>Click a node to edit</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedNode?.data?.componentName && (
|
||||
<div className="right-block">
|
||||
<div className="right-block-header">Item details</div>
|
||||
<div className="right-block-body">
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : componentData ? (
|
||||
<>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<strong style={{ display: 'block', marginBottom: '4px' }}>Component name:</strong>
|
||||
{editingComponentName ? (
|
||||
<input
|
||||
type="text"
|
||||
value={tempComponentName}
|
||||
onChange={(e) => setTempComponentName(e.target.value)}
|
||||
onBlur={handleSaveName}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
style={{ width: '100%', padding: '4px', fontSize: '0.85em', boxSizing: 'border-box' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: '3px',
|
||||
display: 'inline-block',
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
onClick={handleStartEditName}
|
||||
title="Click to edit"
|
||||
>
|
||||
{currentComponentDisplayName || componentData.name} ✎
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p style={{ wordBreak: 'break-all', whiteSpace: 'normal', overflowWrap: 'break-word', marginTop: '8px' }}>
|
||||
<strong>PDK name:</strong><br />{componentData.name}
|
||||
</p>
|
||||
<p><strong>Description:</strong><br />
|
||||
Foundry: {componentData.foundry}<br />
|
||||
Process: {componentData.process}<br />
|
||||
Year: {componentData.year}<br />
|
||||
Designer: {componentData.designer}
|
||||
</p>
|
||||
<p><strong>PDK:</strong> (string)</p>
|
||||
<p><strong>Ports:</strong></p>
|
||||
<ul style={{ paddingLeft: 20, margin: 0 }}>
|
||||
{componentData.ports && Object.entries(componentData.ports).map(([portName, portInfo]) => (
|
||||
<li key={portName} style={{ letterSpacing: '0.5px' }}>{portName}: {formatPort(portInfo)}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p><strong>Image:</strong></p>
|
||||
<div style={{
|
||||
border: '1px dashed #ccc', width: '100%', height: 80,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#999', background: '#fcfcfc', marginTop: 4,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<img
|
||||
src={`/api/component/${encodeURIComponent(componentData.name)}/image`}
|
||||
alt="Component layout"
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', cursor: 'pointer' }}
|
||||
onClick={() => setEnlarged(`/api/component/${encodeURIComponent(componentData.name)}/image`)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.parentElement.innerHTML = 'No image available';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p style={{ color: '#999' }}>No data</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="right-block" style={{ marginTop: 'auto' }}>
|
||||
<div className="right-block-header">Function block to be explored</div>
|
||||
<div className="right-block-body placeholder-block">Reserved for future functionality</div>
|
||||
</div>
|
||||
|
||||
{enlarged && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.8)', zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'zoom-out',
|
||||
}}
|
||||
onClick={() => setEnlarged(null)}
|
||||
>
|
||||
<img
|
||||
src={enlarged}
|
||||
alt="Enlarged layout"
|
||||
style={{ maxWidth: '90%', maxHeight: '90%', objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
const prev = prevProps.selectedNode;
|
||||
const next = nextProps.selectedNode;
|
||||
if (prev?.id !== next?.id) return false;
|
||||
if (prev?.position?.x !== next?.position?.x) return false;
|
||||
if (prev?.position?.y !== next?.position?.y) return false;
|
||||
if (prev?.data?.rotation !== next?.data?.rotation) return false;
|
||||
if (prev?.data?.componentName !== next?.data?.componentName) return false;
|
||||
if (prev?.data?.componentDisplayName !== next?.data?.componentDisplayName) return false;
|
||||
if (prevProps.width !== nextProps.width) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const ResizeHandle = ({ onMouseDown }) => (
|
||||
<div
|
||||
onMouseDown={onMouseDown}
|
||||
style={{
|
||||
width: 6, cursor: 'col-resize', background: 'transparent',
|
||||
transition: 'background 0.2s', zIndex: 5, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = '#ccc'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||
/>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
|
||||
const [library, setLibrary] = useState(null);
|
||||
const [treeKey, setTreeKey] = useState(0);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const treeContainerRef = useRef(null);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(240);
|
||||
const [rightWidth, setRightWidth] = useState(220);
|
||||
const [dragging, setDragging] = useState(null);
|
||||
|
||||
const [gridSnap, setGridSnap] = useState(false);
|
||||
|
||||
const componentCounterRef = useRef(1);
|
||||
|
||||
const generateComponentDisplayName = useCallback(() => {
|
||||
const name = `component_${componentCounterRef.current}`;
|
||||
componentCounterRef.current += 1;
|
||||
return name;
|
||||
}, []);
|
||||
|
||||
const renameComponent = useCallback((nodeId, newComponentDisplayName) => {
|
||||
setNodes(nds => nds.map(n => {
|
||||
if (n.id === nodeId) {
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
componentDisplayName: newComponentDisplayName
|
||||
}
|
||||
};
|
||||
}
|
||||
return n;
|
||||
}));
|
||||
}, [setNodes]);
|
||||
|
||||
const fetchLibrary = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/library');
|
||||
const data = await res.json();
|
||||
setLibrary(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch library', err);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => { fetchLibrary(); }, [fetchLibrary]);
|
||||
|
||||
const selectedNode = useMemo(() => nodes.find(n => n.selected), [nodes]);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
if (!type) return;
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const componentDisplayName = generateComponentDisplayName();
|
||||
const newNode = {
|
||||
id: Date.now().toString(),
|
||||
type: 'rotatableNode',
|
||||
position,
|
||||
data: {
|
||||
label: type,
|
||||
componentName: type,
|
||||
rotation: 0,
|
||||
componentDisplayName: componentDisplayName
|
||||
},
|
||||
};
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [setNodes, reactFlowInstance, generateComponentDisplayName]);
|
||||
|
||||
const onConnect = useCallback((connection) => {
|
||||
setEdges((eds) => addEdge({ ...connection, type: 'straight' }, eds));
|
||||
}, [setEdges]);
|
||||
|
||||
const expandAll = useCallback(() => {
|
||||
if (treeContainerRef.current) {
|
||||
treeContainerRef.current.querySelectorAll('details').forEach(d => d.open = true);
|
||||
}
|
||||
}, []);
|
||||
const collapseAll = useCallback(() => setTreeKey(k => k + 1), []);
|
||||
const handleToggle = useCallback(() => {
|
||||
if (expanded) { collapseAll(); setExpanded(false); }
|
||||
else { expandAll(); setExpanded(true); }
|
||||
}, [expanded, expandAll, collapseAll]);
|
||||
|
||||
const handleResizeStart = useCallback((side) => (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(side);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const onMouseMove = (e) => {
|
||||
if (dragging === 'left') {
|
||||
setLeftWidth(Math.min(500, Math.max(150, e.clientX)));
|
||||
} else if (dragging === 'right') {
|
||||
const newWidth = window.innerWidth - e.clientX;
|
||||
setRightWidth(Math.min(500, Math.max(150, newWidth)));
|
||||
}
|
||||
};
|
||||
const onMouseUp = () => setDragging(null);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}, [dragging]);
|
||||
|
||||
const toggleGridSnap = useCallback(() => {
|
||||
setGridSnap(prev => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%', userSelect: dragging ? 'none' : 'auto' }}>
|
||||
<LeftPanel
|
||||
library={library} treeKey={treeKey} expanded={expanded}
|
||||
onToggle={handleToggle} treeRef={treeContainerRef} width={leftWidth}
|
||||
/>
|
||||
<ResizeHandle onMouseDown={handleResizeStart('left')} />
|
||||
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: 10, right: 10, zIndex: 10,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: '0.85em', fontWeight: 'bold', fontFamily: "'Sofia Pro', sans-serif",
|
||||
color: '#333', userSelect: 'none'
|
||||
}}>Grid lock</span>
|
||||
<div
|
||||
onClick={toggleGridSnap}
|
||||
style={{
|
||||
width: 48, height: 24, borderRadius: 12,
|
||||
background: gridSnap ? '#28a745' : '#ccc',
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center',
|
||||
padding: '0 2px', transition: 'background 0.3s',
|
||||
boxShadow: 'inset 0 1px 3px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 20, height: 20, borderRadius: '50%',
|
||||
background: '#fff', boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
||||
transform: gridSnap ? 'translateX(24px)' : 'translateX(0)',
|
||||
transition: 'transform 0.2s',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={{ rotatableNode: RotatableNode }}
|
||||
fitView
|
||||
snapToGrid={gridSnap}
|
||||
snapGrid={[10, 10]}
|
||||
nodesDraggable={true}
|
||||
nodesConnectable={true}
|
||||
elementsSelectable={true}
|
||||
connectionRadius={50}
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
<ResizeHandle onMouseDown={handleResizeStart('right')} />
|
||||
<RightPanel
|
||||
selectedNode={selectedNode}
|
||||
width={rightWidth}
|
||||
onRenameComponent={renameComponent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<ReactFlowProvider>
|
||||
<App />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
{% endraw %}
|
||||
@@ -60,18 +60,6 @@ assert(
|
||||
serverPy.includes('svg_url'),
|
||||
'save-layout response should include an svg_url for the new layout tab'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('svg_ready') &&
|
||||
serverPy.includes('svg_version') &&
|
||||
serverPy.includes('file_version(svg_path)') &&
|
||||
serverPy.includes("url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version)"),
|
||||
'save-layout response should only expose a versioned SVG URL after the preview file is ready'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('temp_svg_path') &&
|
||||
serverPy.includes('os.replace(temp_svg_path, svg_path)'),
|
||||
'save-layout should publish generated SVG previews atomically instead of serving partially written files'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('RouterStackUnavailable') &&
|
||||
serverPy.includes('except RouterStackUnavailable as e') &&
|
||||
|
||||
@@ -32,13 +32,6 @@ assert(
|
||||
canvasHtml.includes('svg_url'),
|
||||
'Build Layout should use the backend svg_url response'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('result.svg_ready && result.svg_url') &&
|
||||
canvasHtml.includes('buildLayoutRequestRef') &&
|
||||
canvasHtml.includes('buildLayoutBusyRef') &&
|
||||
canvasHtml.includes("cache: 'no-store'"),
|
||||
'Build Layout should wait for a ready, versioned SVG response and prevent stale duplicate preview updates'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('result.preview_error') &&
|
||||
canvasHtml.includes('Preview skipped: '),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# work log
|
||||
|
||||
1.Fixed an issue where switching between different tabs would automatically reset the zoom level.
|
||||
|
||||
2.Fixed an port width mismatch in YAML bundles.
|
||||
|
||||
3.Fixed the issue where SVG were displaying in incorrect positions.
|
||||
|
||||
4.Fixed the abnormal port shift after rotation.
|
||||
|
||||
5.Fixed the abnormal position of individual ports.
|
||||
Reference in New Issue
Block a user