updated with github #5
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.
+107
-2
@@ -1,6 +1,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Description: Backend integration wrapper for project GDS generation and build result handling.
|
||||
# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _build_with_gdstk, _import_public_gds, _build_with_nazca, _safe_cell_name, _library_cell_by_name, _number
|
||||
# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _cells_have_elements, _build_with_gdstk, _import_public_gds, _build_with_nazca, _build_nazca_element_cells, _build_nazca_element_cell, _element_port_offset, _safe_cell_name, _library_cell_by_name, _int, _number
|
||||
# Developer : Qin Yue @ 2026
|
||||
# Organization : OptiHK Limited
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -58,7 +58,19 @@ def build_project_gds(
|
||||
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# gdstk is the preferred fallback; Nazca remains a secondary fallback for
|
||||
# Port/Anchor elements are logical pin-bearing devices. Use Nazca for these
|
||||
# layouts so each element can be a placed cell with local nd.Pin metadata.
|
||||
if _cells_have_elements(cells):
|
||||
try:
|
||||
return _build_with_nazca(cells, output_path, registry)
|
||||
except ImportError as nazca_error:
|
||||
raise RuntimeError(
|
||||
"Build GDS with Port/Anchor elements requires Nazca so element cells can preserve nd.Pin metadata. "
|
||||
f"Nazca import failed: {nazca_error}"
|
||||
) from nazca_error
|
||||
|
||||
# gdstk is the preferred fallback for placement-only projects without
|
||||
# Port/Anchor element pin metadata; Nazca remains a secondary fallback for
|
||||
# environments where gdstk is not installed.
|
||||
try:
|
||||
return _build_with_gdstk(cells, output_path, registry)
|
||||
@@ -132,6 +144,16 @@ def _cells_have_links(cells: Dict[str, dict]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _cells_have_elements(cells: Dict[str, dict]) -> bool:
|
||||
"""Detect whether any saved cell contains Port/Anchor element objects."""
|
||||
for data in cells.values():
|
||||
elements = data.get("elements") or {}
|
||||
for element in elements.values():
|
||||
if str((element or {}).get("type") or "").lower() in {"port", "anchor"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult:
|
||||
"""Assemble a project GDS with gdstk when Nazca or routed building is unavailable."""
|
||||
import gdstk
|
||||
@@ -196,6 +218,7 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
|
||||
# is exported as the top-level GDS.
|
||||
for cell_name in ordered_names:
|
||||
data = cells[cell_name]
|
||||
element_cells = _build_nazca_element_cells(nd, cell_name, data.get("elements") or {}, built_cells)
|
||||
with nd.Cell(cell_name) as current_cell:
|
||||
for instance_name, instance in (data.get("instances") or {}).items():
|
||||
component = str(instance.get("component") or "")
|
||||
@@ -211,6 +234,12 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
|
||||
continue
|
||||
loaded = nd.load_gds(asset.gds_path)
|
||||
loaded.put(x, y, rotation)
|
||||
for element_name, element_cell in element_cells.items():
|
||||
element = (data.get("elements") or {}).get(element_name) or {}
|
||||
x = _number(element.get("x"))
|
||||
y = _number(element.get("y"))
|
||||
rotation = _number(element.get("angle"))
|
||||
element_cell.put(x, y, rotation)
|
||||
built_cells[cell_name] = current_cell
|
||||
|
||||
top_name = ordered_names[-1]
|
||||
@@ -218,6 +247,72 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
|
||||
return BuildResult(output_path=output_path, engine="nazca", cells_built=ordered_names, warnings=warnings)
|
||||
|
||||
|
||||
def _build_nazca_element_cells(nd, parent_cell_name: str, elements: dict, existing_cells: dict) -> Dict[str, object]:
|
||||
"""Build reusable Nazca cells for built-in Port and Anchor element objects."""
|
||||
element_cells = {}
|
||||
known_cells = dict(existing_cells or {})
|
||||
for element_name, element in (elements or {}).items():
|
||||
element_type = str((element or {}).get("type") or "").lower()
|
||||
if element_type not in {"port", "anchor"}:
|
||||
continue
|
||||
raw_cell_name = f"{parent_cell_name}_{element_name}_{element_type}"
|
||||
cell_name = _safe_cell_name(raw_cell_name, {**known_cells, **element_cells})
|
||||
element_cell = _build_nazca_element_cell(nd, cell_name, {**(element or {}), "name": str(element_name)})
|
||||
element_cells[str(element_name)] = element_cell
|
||||
return element_cells
|
||||
|
||||
|
||||
def _build_nazca_element_cell(nd, cell_name: str, element: dict):
|
||||
"""Create one local Port or Anchor cell whose pins are placed in local coordinates."""
|
||||
element = element or {}
|
||||
element_type = str(element.get("type") or "").lower()
|
||||
width = _number(element.get("width"), 0.5)
|
||||
port_number = max(1, _int(element.get("pin_number", element.get("pinNumber", element.get("port_number", element.get("portNumber")))), 1))
|
||||
pitch = _number(element.get("pitch"), 10.0)
|
||||
with nd.Cell(name=cell_name) as element_cell:
|
||||
if element_type == "port":
|
||||
for index, pin_name in enumerate(_element_pin_names(cell_name, element, port_number, "port")):
|
||||
y = 0.0 if port_number == 1 else _element_port_offset(index, port_number, pitch)
|
||||
nd.Pin(pin_name, width=width).put(0.0, y, 180.0)
|
||||
return element_cell
|
||||
|
||||
anchor_pin_names = _element_pin_names(cell_name, element, port_number, "anchor")
|
||||
for index in range(port_number):
|
||||
y = -15.0 + _element_port_offset(index, port_number, pitch)
|
||||
nd.Pin(anchor_pin_names[index * 2], width=width).put(0.0, y, 180.0)
|
||||
nd.Pin(anchor_pin_names[index * 2 + 1], width=width).put(0.0, y, 0.0)
|
||||
return element_cell
|
||||
|
||||
|
||||
def _element_port_offset(index: int, count: int, pitch: float) -> float:
|
||||
"""Return the local y offset for one repeated Port/Anchor pin."""
|
||||
return ((max(1, count) - 1) / 2 - index) * pitch
|
||||
|
||||
|
||||
def _element_pin_names(cell_name: str, element: dict, count: int, element_type: str) -> List[str]:
|
||||
"""Return local element pin names, using YAML overrides when present."""
|
||||
raw_pins = [pin for pin in (element.get("pins") or []) if isinstance(pin, dict)]
|
||||
default_base = _safe_pin_name(str(element.get("name") or cell_name).replace("_port", "").replace("_anchor", ""))
|
||||
roles = []
|
||||
if element_type == "port":
|
||||
roles = [f"io{index + 1}" for index in range(count)]
|
||||
else:
|
||||
for index in range(count):
|
||||
roles.extend([f"a{index + 1}", f"b{index + 1}"])
|
||||
by_role = {str(pin.get("role") or ""): str(pin.get("name") or "") for pin in raw_pins}
|
||||
by_index = [str(pin.get("name") or "") for pin in raw_pins]
|
||||
names = []
|
||||
for index, role in enumerate(roles):
|
||||
name = by_role.get(role) or (by_index[index] if index < len(by_index) else "") or f"{default_base}_{role}"
|
||||
names.append(_safe_pin_name(name))
|
||||
return names
|
||||
|
||||
|
||||
def _safe_pin_name(name: str) -> str:
|
||||
"""Generate a backend-safe pin name for local Nazca element pins."""
|
||||
return "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in str(name or "pin"))
|
||||
|
||||
|
||||
def _safe_cell_name(name: str, existing: dict) -> str:
|
||||
"""Generate a backend-safe unique cell name for GDS/Nazca libraries."""
|
||||
base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell"
|
||||
@@ -238,6 +333,16 @@ def _library_cell_by_name(library, name: str):
|
||||
return None
|
||||
|
||||
|
||||
def _int(value, default=0) -> int:
|
||||
"""Convert integer YAML values with a stable default."""
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _number(value, default=0.0) -> float:
|
||||
"""Convert numeric YAML values to floats with a stable default."""
|
||||
try:
|
||||
|
||||
+18
-1
@@ -107,7 +107,24 @@ class PdkRegistry:
|
||||
if not yaml_path:
|
||||
return None
|
||||
with open(yaml_path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
data = yaml.safe_load(file) or {}
|
||||
return self._normalize_pdk_pins(data)
|
||||
|
||||
def _normalize_pdk_pins(self, data: dict) -> dict:
|
||||
"""Treat legacy PDK `ports` metadata as `pins` inside approved PDK roots."""
|
||||
if (
|
||||
self._allows_legacy_ports_as_pins()
|
||||
and isinstance(data, dict)
|
||||
and "pins" not in data
|
||||
and isinstance(data.get("ports"), dict)
|
||||
):
|
||||
data = dict(data)
|
||||
data["pins"] = data["ports"]
|
||||
return data
|
||||
|
||||
def _allows_legacy_ports_as_pins(self) -> bool:
|
||||
normalized = self.public_root.replace("\\", "/").lower()
|
||||
return "/opt_pdk_public/" in f"{normalized}/" or "/opt_pdk_atlas/" in f"{normalized}/"
|
||||
|
||||
def _inside_root(self, path: str) -> bool:
|
||||
"""Check that a candidate asset path remains inside the permitted PDK root."""
|
||||
|
||||
+7
-1
@@ -360,7 +360,13 @@ def readCompYaml(compName, comps_root=None):
|
||||
if ymlFiles:
|
||||
ymlPath = os.path.join(root, ymlFiles[0])
|
||||
with open(ymlPath, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
data = yaml.safe_load(f) or {}
|
||||
normalized_root = os.path.abspath(search_root).replace("\\", "/").lower()
|
||||
pdk_ports_allowed = "/opt_pdk_public/" in f"{normalized_root}/" or "/opt_pdk_atlas/" in f"{normalized_root}/"
|
||||
if pdk_ports_allowed and isinstance(data, dict) and "pins" not in data and isinstance(data.get("ports"), dict):
|
||||
data = dict(data)
|
||||
data["pins"] = data["ports"]
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
|
||||
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.
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "mxpic_project_1",
|
||||
"name": "MZM_TX",
|
||||
"technology": "Silterra/EMO1_2ML_CU_Al_RDL"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 465 KiB |
@@ -0,0 +1,102 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 5000
|
||||
height: 5000
|
||||
project: MZM_TX
|
||||
name: MZM_TX
|
||||
type: project
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
pins: []
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
Spliter_1x4:
|
||||
component: Spliter_1x4
|
||||
x: 330.0
|
||||
y: -630.0
|
||||
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: 750.0
|
||||
y: -460.0
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
EC_1:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/edge_couplers/EC_SiN400_1310_0p5dB_L935_A0_QY_202604
|
||||
x: 0.0
|
||||
y: -920.0
|
||||
rotation: 180.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
anchor_2:
|
||||
type: anchor
|
||||
x: 470.0
|
||||
y: -840.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: anchor_2_a1
|
||||
role: a1
|
||||
- name: anchor_2_b1
|
||||
role: b1
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: MZM_1:a1
|
||||
to: Spliter_1x4:OutUp_io2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: Spliter_1x4:OutUp_io1
|
||||
to: MZM_1:a2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: anchor_2:anchor_2_b1
|
||||
to: EC_1:a1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: anchor_2:anchor_2_a1
|
||||
to: Spliter_1x4:Input_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
+152
-106
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 179 KiB |
@@ -0,0 +1,192 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 500
|
||||
height: 500
|
||||
project: MZM_TX
|
||||
name: Spliter_1x4
|
||||
type: composite
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
pins:
|
||||
- name: Input_io1
|
||||
layer: WG_CORE
|
||||
element: Input
|
||||
pin: io1
|
||||
x: 10.0
|
||||
y: -110.0
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
- name: OutUp_io1
|
||||
layer: WG_CORE
|
||||
element: OutUp
|
||||
pin: io1
|
||||
x: 335.0
|
||||
y: -20.0
|
||||
angle: 90.0
|
||||
width: 0.5
|
||||
- name: OutUp_io2
|
||||
layer: WG_CORE
|
||||
element: OutUp
|
||||
pin: io2
|
||||
x: 325.0
|
||||
y: -20.0
|
||||
angle: 90.0
|
||||
width: 0.5
|
||||
- name: port_4_io1
|
||||
layer: WG_CORE
|
||||
element: port_4
|
||||
pin: io1
|
||||
x: 350.0
|
||||
y: -180.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
MMI_2:
|
||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||
x: 90.0
|
||||
y: -110.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: 230.0
|
||||
y: -90.0
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
Input:
|
||||
type: port
|
||||
x: 10.0
|
||||
y: -110.0
|
||||
angle: 0.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: Input_io1
|
||||
role: io1
|
||||
OutUp:
|
||||
type: port
|
||||
x: 330.0
|
||||
y: -20.0
|
||||
angle: -90.0
|
||||
pin_number: 2
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: OutUp_io1
|
||||
role: io1
|
||||
- name: OutUp_io2
|
||||
role: io2
|
||||
port_4:
|
||||
type: port
|
||||
x: 350.0
|
||||
y: -180.0
|
||||
angle: 180.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: port_4_io1
|
||||
role: io1
|
||||
anchor_1:
|
||||
type: anchor
|
||||
x: 250.0
|
||||
y: -150.0
|
||||
angle: 90.0
|
||||
pin_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
pins:
|
||||
- name: anchor_1_a1
|
||||
role: a1
|
||||
- name: anchor_1_b1
|
||||
role: b1
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: MMI_2:a1
|
||||
to: Input:Input_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:a1
|
||||
to: MMI_2:b1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:b1
|
||||
to: OutUp:OutUp_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:b1
|
||||
to: OutUp:OutUp_io2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_3:b2
|
||||
to: OutUp:OutUp_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:b2
|
||||
to: anchor_1:anchor_1_b1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: anchor_1:anchor_1_a1
|
||||
to: port_4:port_4_io1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: anchor_1:anchor_1_a1
|
||||
to: anchor_1:anchor_1_b1
|
||||
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: 78 KiB |
@@ -1,117 +0,0 @@
|
||||
# =============================================
|
||||
# mxPIC Cell/Project Definition File
|
||||
# =============================================
|
||||
schema_version: "2.0.0"
|
||||
kind: cell
|
||||
coordinate_system: gds_y_up
|
||||
canvas_size:
|
||||
width: 5000
|
||||
height: 500
|
||||
project: mxpic_project_1
|
||||
name: canvas_1
|
||||
type: composite
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
ports:
|
||||
- name: port
|
||||
layer: WG_CORE
|
||||
x: 103.5
|
||||
y: -127.3
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
- name: port_1
|
||||
layer: WG_CORE
|
||||
x: 108.7
|
||||
y: -252.6
|
||||
angle: 180.0
|
||||
width: 0.5
|
||||
- name: port_2
|
||||
layer: WG_CORE
|
||||
x: 497.4
|
||||
y: -131.6
|
||||
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: 177.9
|
||||
y: -252.1
|
||||
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: 356.7
|
||||
y: -142.9
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
port:
|
||||
type: port
|
||||
x: 103.5
|
||||
y: -127.3
|
||||
angle: 0.0
|
||||
port_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
port_1:
|
||||
type: port
|
||||
x: 108.7
|
||||
y: -252.6
|
||||
angle: 0.0
|
||||
port_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
port_2:
|
||||
type: port
|
||||
x: 497.4
|
||||
y: -131.6
|
||||
angle: 180.0
|
||||
port_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: MMI_1:a1
|
||||
to: port_1:port
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_1:b1
|
||||
to: MMI_2:a1
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
- from: MMI_2:b1
|
||||
to: port_2:port
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 195 KiB |
@@ -1,71 +0,0 @@
|
||||
# =============================================
|
||||
# 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: mxpic_project_1
|
||||
type: project
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
ports:
|
||||
- name: port
|
||||
layer: WG_CORE
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 0.0
|
||||
width: 0.5
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:
|
||||
canvas_1:
|
||||
component: canvas_1
|
||||
x: 476.9
|
||||
y: -1056.4
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
canvas_1_1:
|
||||
component: canvas_1
|
||||
x: 1139.8
|
||||
y: -958.5
|
||||
rotation: 0.0
|
||||
flip: 0
|
||||
flop: 0
|
||||
mirror: false
|
||||
settings:
|
||||
length:
|
||||
|
||||
elements:
|
||||
port:
|
||||
type: port
|
||||
x: 50.0
|
||||
y: -150.0
|
||||
angle: 180.0
|
||||
port_number: 1
|
||||
pitch: 10
|
||||
layer: WG_CORE
|
||||
width: 0.5
|
||||
description: ""
|
||||
|
||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||
bundles:
|
||||
output_bus:
|
||||
routing_type: euler_bend
|
||||
links:
|
||||
- from: canvas_1_1:port_1
|
||||
to: canvas_1:port_2
|
||||
xsection: strip
|
||||
family: optical
|
||||
width: 0.45
|
||||
radius: 10
|
||||
routing_type: euler_bend
|
||||
Binary file not shown.
+177
-39
@@ -22,8 +22,11 @@
|
||||
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
|
||||
// Base visual diameter and hit area used for port and anchor handles.
|
||||
const PORT_NODE_SIZE = 30;
|
||||
// Narrow anchor body width used in the canvas visual representation.
|
||||
const ANCHOR_NODE_WIDTH = 8;
|
||||
const PORT_LABEL_MIN_CHARS = 5;
|
||||
const PORT_LABEL_CHAR_WIDTH = 7;
|
||||
const PORT_LABEL_HORIZONTAL_PADDING = 12;
|
||||
// Anchor body width used in the canvas visual representation.
|
||||
const ANCHOR_NODE_WIDTH = 16;
|
||||
// Default spacing between repeated anchor or port pins.
|
||||
const DEFAULT_ELEMENT_PITCH = 10;
|
||||
// Defines built-in port and anchor element metadata before per-node expansion.
|
||||
@@ -477,7 +480,7 @@
|
||||
const nextY = x * sin + y * cos;
|
||||
x = nextX;
|
||||
y = nextY;
|
||||
angle += rotation;
|
||||
angle -= rotation;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -489,16 +492,86 @@
|
||||
};
|
||||
|
||||
// Create ordered React Flow handles for all ports on a single visual side.
|
||||
const buildSideHandles = (ports, side) => {
|
||||
const clampPercent = (value) => roundPercent(Math.min(100, Math.max(0, value)));
|
||||
|
||||
const createCoordinateMetrics = (ports, boxSize) => {
|
||||
const width = positiveNumber(boxSize && boxSize.width);
|
||||
const height = positiveNumber(boxSize && boxSize.height);
|
||||
if (!width || !height) return null;
|
||||
const values = { x: [], y: [] };
|
||||
ports.forEach(port => {
|
||||
const info = port && port.info;
|
||||
const x = Number(info && info.x);
|
||||
const y = Number(info && info.y);
|
||||
if (Number.isFinite(x)) values.x.push(x);
|
||||
if (Number.isFinite(y)) values.y.push(y);
|
||||
});
|
||||
return { width, height, values };
|
||||
};
|
||||
|
||||
const coordinateAxisMode = (axisValues, size) => {
|
||||
if (!axisValues || axisValues.length === 0 || !size) return 'fallback';
|
||||
const min = Math.min(...axisValues);
|
||||
const max = Math.max(...axisValues);
|
||||
const epsilon = Math.max(0.001, size * 0.001);
|
||||
if (Math.abs(max - min) <= epsilon) {
|
||||
return Math.abs(max) <= epsilon ? 'centered' : 'fallback';
|
||||
}
|
||||
if (min < -epsilon && max <= size / 2 + epsilon && min >= -size / 2 - epsilon) {
|
||||
return 'centered';
|
||||
}
|
||||
if (min >= -epsilon && max <= size + epsilon) {
|
||||
return 'positive';
|
||||
}
|
||||
return 'fallback';
|
||||
};
|
||||
|
||||
const coordinatePercent = (info, axis, metrics) => {
|
||||
if (!metrics) return null;
|
||||
const value = Number(info && info[axis]);
|
||||
if (!Number.isFinite(value)) return null;
|
||||
const size = axis === 'x' ? metrics.width : metrics.height;
|
||||
const mode = coordinateAxisMode(metrics.values[axis], size);
|
||||
if (mode === 'centered') {
|
||||
return axis === 'x'
|
||||
? clampPercent(50 + (value / size) * 100)
|
||||
: clampPercent(50 - (value / size) * 100);
|
||||
}
|
||||
if (mode === 'positive') {
|
||||
return axis === 'x'
|
||||
? clampPercent((value / size) * 100)
|
||||
: clampPercent(100 - (value / size) * 100);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildSideHandles = (ports, side, metrics) => {
|
||||
const vertical = side === 'left' || side === 'right';
|
||||
|
||||
return ports.map((port, index) => {
|
||||
const explicitPercent = Number(port.info && port.info.handlePercent);
|
||||
const percent = Number.isFinite(explicitPercent) ? explicitPercent : fallbackPercent(index, ports.length);
|
||||
const exactPercent = coordinatePercent(port.info, vertical ? 'y' : 'x', metrics);
|
||||
const percent = Number.isFinite(explicitPercent)
|
||||
? explicitPercent
|
||||
: exactPercent !== null
|
||||
? exactPercent
|
||||
: fallbackPercent(index, ports.length);
|
||||
const percentValue = `${percent}%`;
|
||||
const style = vertical
|
||||
? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' }
|
||||
: { left: percentValue, transform: side === 'top' ? 'translate(-50%, -50%)' : 'translate(-50%, 50%)' };
|
||||
? {
|
||||
left: side === 'left' ? 0 : '100%',
|
||||
right: 'auto',
|
||||
top: percentValue,
|
||||
bottom: 'auto',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}
|
||||
: {
|
||||
left: percentValue,
|
||||
right: 'auto',
|
||||
top: side === 'top' ? 0 : '100%',
|
||||
bottom: 'auto',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
|
||||
return {
|
||||
name: port.name,
|
||||
@@ -511,13 +584,18 @@
|
||||
|
||||
// Group transformed ports into canvas handles with side and position styling.
|
||||
const buildPortHandles = (ports, transform) => {
|
||||
const options = transform || {};
|
||||
const grouped = { left: [], right: [], top: [], bottom: [] };
|
||||
const allPorts = [];
|
||||
Object.entries(ports || {}).forEach(([name, info]) => {
|
||||
if (name === 'a0' || name === 'b0') return;
|
||||
const transformedInfo = transformPortInfo(info, transform);
|
||||
const transformedInfo = transformPortInfo(info, options);
|
||||
const side = portSideFromAngle(transformedInfo.a);
|
||||
grouped[side].push({ name, info: transformedInfo });
|
||||
const port = { name, info: transformedInfo };
|
||||
grouped[side].push(port);
|
||||
allPorts.push(port);
|
||||
});
|
||||
const metrics = createCoordinateMetrics(allPorts, options.boxSize);
|
||||
|
||||
Object.values(grouped).forEach(sidePorts => {
|
||||
sidePorts.sort((a, b) => {
|
||||
@@ -529,10 +607,10 @@
|
||||
});
|
||||
|
||||
return [
|
||||
...buildSideHandles(grouped.left, 'left'),
|
||||
...buildSideHandles(grouped.right, 'right'),
|
||||
...buildSideHandles(grouped.top, 'top'),
|
||||
...buildSideHandles(grouped.bottom, 'bottom')
|
||||
...buildSideHandles(grouped.left, 'left', metrics),
|
||||
...buildSideHandles(grouped.right, 'right', metrics),
|
||||
...buildSideHandles(grouped.top, 'top', metrics),
|
||||
...buildSideHandles(grouped.bottom, 'bottom', metrics)
|
||||
];
|
||||
};
|
||||
|
||||
@@ -615,6 +693,35 @@
|
||||
return name || (node && node.id) || 'port';
|
||||
};
|
||||
|
||||
const pinRoleFromElementPortName = (elementType, portName) => {
|
||||
const name = String(portName || '');
|
||||
if (elementType === 'anchor') {
|
||||
const anchorMatch = name.match(/^([ab])(\d+)$/);
|
||||
return anchorMatch ? `${anchorMatch[1]}${anchorMatch[2]}` : name;
|
||||
}
|
||||
const portMatch = name.match(/^port_(\d+)$/);
|
||||
return portMatch ? `io${portMatch[1]}` : 'io1';
|
||||
};
|
||||
|
||||
const defaultElementPinName = (elementName, role) => `${elementName}_${role}`;
|
||||
|
||||
const getElementPinName = (node, portName) => {
|
||||
const data = (node && node.data) || {};
|
||||
const elementType = data.elementType === 'anchor' ? 'anchor' : 'port';
|
||||
const elementName = getNodePortName(node);
|
||||
const role = pinRoleFromElementPortName(elementType, portName);
|
||||
return (data.pinNames && data.pinNames[role]) || defaultElementPinName(elementName, role);
|
||||
};
|
||||
|
||||
const buildElementPinEntries = (node) => {
|
||||
const data = (node && node.data) || {};
|
||||
const elementType = data.elementType === 'anchor' ? 'anchor' : 'port';
|
||||
return Object.keys(buildElementPorts(elementType, data)).map(portName => {
|
||||
const role = pinRoleFromElementPortName(elementType, portName);
|
||||
return { role, name: getElementPinName(node, portName) };
|
||||
});
|
||||
};
|
||||
|
||||
// Detect standalone port nodes that become top-level layout ports.
|
||||
const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode');
|
||||
// Detect built-in port or anchor nodes for element YAML export.
|
||||
@@ -658,8 +765,13 @@
|
||||
const portNumber = normalizePortNumber(data && data.portNumber);
|
||||
const pitch = normalizePitch(data && data.pitch);
|
||||
const handleClearance = Math.max(pitch, 14);
|
||||
const portDisplayName = String((data && (data.portName || data.componentDisplayName || data.label)) || 'port');
|
||||
const portWidth = Math.max(
|
||||
PORT_NODE_SIZE,
|
||||
PORT_LABEL_HORIZONTAL_PADDING + Math.max(PORT_LABEL_MIN_CHARS, portDisplayName.length) * PORT_LABEL_CHAR_WIDTH
|
||||
);
|
||||
return {
|
||||
width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : PORT_NODE_SIZE,
|
||||
width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : portWidth,
|
||||
height: Math.max(PORT_NODE_SIZE, PORT_NODE_SIZE + Math.max(0, portNumber - 1) * handleClearance)
|
||||
};
|
||||
};
|
||||
@@ -695,7 +807,7 @@
|
||||
if (portNumber > 1) {
|
||||
const entries = [];
|
||||
Array.from({ length: portNumber }, (_, index) => {
|
||||
const y = -PORT_NODE_SIZE / 2 + elementPortOffset(index, portNumber, pitch);
|
||||
const y = elementPortOffset(index, portNumber, pitch);
|
||||
entries.push([`a${index + 1}`, { x: 0, y, a: 180, width }]);
|
||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
||||
});
|
||||
@@ -771,40 +883,41 @@
|
||||
};
|
||||
};
|
||||
|
||||
// Flip an internal standalone Port angle into the outward-facing cell port
|
||||
// angle used when this canvas is placed as a component elsewhere.
|
||||
// Export standalone Port pins as the outward-facing pin angle.
|
||||
const externalPortAngle = (angle) => normalizeAngle(Number(angle ?? 0) + 180);
|
||||
|
||||
// Convert standalone port nodes into page-level layout ports.
|
||||
const buildPageComponentPorts = (port, nodes) => {
|
||||
// Convert standalone port nodes into page-level layout pins.
|
||||
const buildPageComponentPins = (port, nodes) => {
|
||||
const portNodes = (nodes || []).filter(isPortElementNode);
|
||||
if (portNodes.length > 0) {
|
||||
return portNodes.reduce((ports, node) => {
|
||||
return portNodes.reduce((pins, node) => {
|
||||
const data = node.data || {};
|
||||
const baseName = getNodePortName(node);
|
||||
const elementPorts = buildElementPorts('port', data);
|
||||
const entries = Object.entries(elementPorts);
|
||||
entries.forEach(([portName, portInfo]) => {
|
||||
const exportName = entries.length === 1
|
||||
? baseName
|
||||
: `${baseName}_${portName.replace(/^port_/, '')}`;
|
||||
const exportName = getElementPinName(node, portName);
|
||||
const point = getNodePortCanvasPoint(node, portName) || {
|
||||
x: Number((node.position && node.position.x) || 0),
|
||||
y: Number((node.position && node.position.y) || 0)
|
||||
};
|
||||
ports[exportName] = {
|
||||
pins[exportName] = {
|
||||
element: baseName,
|
||||
pin: pinRoleFromElementPortName('port', portName),
|
||||
x: Number(point.x || 0),
|
||||
y: Number(point.y || 0),
|
||||
a: externalPortAngle(portInfo.a ?? data.angle ?? data.a ?? 0),
|
||||
width: Number(portInfo.width || data.width || 0.5)
|
||||
};
|
||||
});
|
||||
return ports;
|
||||
return pins;
|
||||
}, {});
|
||||
}
|
||||
if (!port) return {};
|
||||
return {
|
||||
port: {
|
||||
port_io1: {
|
||||
element: 'port',
|
||||
pin: 'io1',
|
||||
x: Number(port.x || 0),
|
||||
y: Number(port.y || 0),
|
||||
a: externalPortAngle(port.a || 0),
|
||||
@@ -813,27 +926,34 @@
|
||||
};
|
||||
};
|
||||
|
||||
// Serialize standalone canvas ports into a layout ports YAML section.
|
||||
const buildCanvasPortsYaml = (nodes, fallbackPort) => {
|
||||
const ports = buildPageComponentPorts(fallbackPort, nodes);
|
||||
const entries = Object.entries(ports);
|
||||
if (entries.length === 0) return 'ports: []';
|
||||
// Backward-compatible helper name for callers that still use the old JS API.
|
||||
const buildPageComponentPorts = buildPageComponentPins;
|
||||
|
||||
// Serialize standalone canvas pins into a layout pins YAML section.
|
||||
const buildCanvasPinsYaml = (nodes, fallbackPort) => {
|
||||
const pins = buildPageComponentPins(fallbackPort, nodes);
|
||||
const entries = Object.entries(pins);
|
||||
if (entries.length === 0) return 'pins: []';
|
||||
const sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node]));
|
||||
const lines = entries.map(([name, info]) => {
|
||||
const data = (sourceNodes.get(name) && sourceNodes.get(name).data) || {};
|
||||
const data = (sourceNodes.get(info.element) && sourceNodes.get(info.element).data) || {};
|
||||
const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : '';
|
||||
return `- name: ${name}
|
||||
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
|
||||
element: ${info.element}
|
||||
pin: ${info.pin}
|
||||
x: ${Number(info.x || 0).toFixed(1)}
|
||||
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
||||
angle: ${Number(info.a || 0).toFixed(1)}
|
||||
width: ${Number(info.width || 0.5)}${description}`;
|
||||
});
|
||||
return `ports:\n${lines.join('\n')}`;
|
||||
return `pins:\n${lines.join('\n')}`;
|
||||
};
|
||||
|
||||
const buildCanvasPortsYaml = buildCanvasPinsYaml;
|
||||
|
||||
// Maintain legacy single-port YAML export behavior for older callers.
|
||||
const buildPortsYaml = (port) => buildCanvasPortsYaml([], port);
|
||||
const buildPortsYaml = (port) => buildCanvasPinsYaml([], port);
|
||||
|
||||
// Serialize built-in port and anchor nodes into layout element metadata.
|
||||
const buildElementsYaml = (nodes) => {
|
||||
@@ -845,16 +965,21 @@
|
||||
const angle = data.elementType === 'port' ? data.angle : data.rotation;
|
||||
const portNumber = normalizePortNumber(data.portNumber);
|
||||
const pitch = normalizePitch(data.pitch);
|
||||
const pinLines = buildElementPinEntries(node)
|
||||
.map(pin => ` - name: ${pin.name}\n role: ${pin.role}`)
|
||||
.join('\n');
|
||||
return ` ${name}:
|
||||
type: ${data.elementType}
|
||||
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
|
||||
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
|
||||
angle: ${Number(angle || 0).toFixed(1)}
|
||||
port_number: ${portNumber}
|
||||
pin_number: ${portNumber}
|
||||
pitch: ${Number(pitch)}
|
||||
layer: ${data.layer || 'WG_CORE'}
|
||||
width: ${Number(data.width || 0.5)}
|
||||
description: ${toYamlScalar(data.description || '')}`;
|
||||
description: ${toYamlScalar(data.description || '')}
|
||||
pins:
|
||||
${pinLines}`;
|
||||
});
|
||||
return `elements:\n${lines.join('\n')}`;
|
||||
};
|
||||
@@ -872,8 +997,12 @@
|
||||
const targetNode = nodeMap[edge.target];
|
||||
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
||||
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
||||
const fromPort = edge.sourceHandle || 'unknown';
|
||||
const toPort = edge.targetHandle || 'unknown';
|
||||
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
|
||||
? getElementPinName(sourceNode, edge.sourceHandle)
|
||||
: edge.sourceHandle || 'unknown';
|
||||
const toPort = targetNode && targetNode.data && targetNode.data.elementType
|
||||
? getElementPinName(targetNode, edge.targetHandle)
|
||||
: edge.targetHandle || 'unknown';
|
||||
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
||||
@@ -926,7 +1055,12 @@ ${linksYaml}`;
|
||||
const ports = buildElementPorts('port', node.data);
|
||||
const portInfo = ports && portName ? ports[portName] : ports.port;
|
||||
if (!portInfo) return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
|
||||
const transformedInfo = transformPortInfo(portInfo, { rotation: 0 });
|
||||
const data = node.data || {};
|
||||
const transformedInfo = transformPortInfo(portInfo, {
|
||||
rotation: data.angle ?? data.a ?? data.rotation ?? 0,
|
||||
flip: Boolean(data.flip),
|
||||
flop: Boolean(data.flop)
|
||||
});
|
||||
return {
|
||||
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
|
||||
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
|
||||
@@ -1127,12 +1261,16 @@ ${linksYaml}`;
|
||||
getNodePortCanvasPoint,
|
||||
buildPortHandles,
|
||||
buildElementPorts,
|
||||
buildElementPinEntries,
|
||||
getElementPinName,
|
||||
buildElementBoxSize,
|
||||
buildBasicComponentPorts,
|
||||
getBasicComponentMetadata,
|
||||
buildInstanceYaml,
|
||||
buildInstancesYaml,
|
||||
buildPageComponentPorts,
|
||||
buildPageComponentPins,
|
||||
buildCanvasPinsYaml,
|
||||
buildCanvasPortsYaml,
|
||||
buildBundlesYaml,
|
||||
buildPortsYaml,
|
||||
|
||||
+448
-136
@@ -662,6 +662,18 @@ Organization : OptiHK Limited
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.origin-select-btn {
|
||||
border-color: rgba(45, 212, 191, 0.55);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.origin-select-btn.active {
|
||||
background: rgba(45, 212, 191, 0.16);
|
||||
border-color: var(--accent-green);
|
||||
color: var(--text-main);
|
||||
box-shadow: 0 0 0 1px rgba(45, 212, 191, 0.2), 0 10px 20px rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
|
||||
body.light-mode .mini-btn {
|
||||
background: var(--mini-button-bg);
|
||||
border-color: rgba(30, 48, 69, 0.18);
|
||||
@@ -697,7 +709,10 @@ Organization : OptiHK Limited
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
max-width: calc(100% - 30px);
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
@@ -723,6 +738,71 @@ Organization : OptiHK Limited
|
||||
color: #102033;
|
||||
}
|
||||
|
||||
.coordinate-readout {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 270px;
|
||||
justify-content: center;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: rgba(13, 22, 38, 0.9);
|
||||
color: var(--text-main);
|
||||
box-shadow: 0 14px 28px var(--shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
font: 600 0.62rem/1 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.coordinate-readout span {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
body.light-mode .coordinate-readout {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-color: rgba(30, 48, 69, 0.16);
|
||||
box-shadow: 0 14px 28px rgba(18, 32, 51, 0.12);
|
||||
}
|
||||
|
||||
.origin-crosshair {
|
||||
position: absolute;
|
||||
z-index: 18;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.origin-crosshair::before,
|
||||
.origin-crosshair::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 10px rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
|
||||
.origin-crosshair::before {
|
||||
width: 22px;
|
||||
height: 1px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.origin-crosshair::after {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.build-layout-btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
@@ -1195,6 +1275,34 @@ Organization : OptiHK Limited
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.port-pin-label {
|
||||
position: absolute;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||
font-size: 0.34rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--port-label-text);
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.port-pin-label span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.anchor-node-shell {
|
||||
position: relative;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
}
|
||||
|
||||
.anchor-visual-body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.build-progress {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -1459,11 +1567,12 @@ Organization : OptiHK Limited
|
||||
calculateCompositeBoxSize,
|
||||
buildPortHandles,
|
||||
buildElementPorts,
|
||||
getElementPinName,
|
||||
buildElementBoxSize,
|
||||
getBasicComponentMetadata,
|
||||
buildInstancesYaml,
|
||||
buildPageComponentPorts,
|
||||
buildCanvasPortsYaml,
|
||||
buildCanvasPinsYaml,
|
||||
buildElementsYaml,
|
||||
buildBundlesYaml: buildRouteBundlesYaml,
|
||||
normalizeAngle,
|
||||
@@ -1599,9 +1708,9 @@ Organization : OptiHK Limited
|
||||
}, [id, data.ports, data.componentName, data.boxSize]);
|
||||
|
||||
const baseHandleStyle = {
|
||||
width: 8, height: 8,
|
||||
width: 6, height: 6,
|
||||
background: 'var(--bg-main)',
|
||||
border: '2px solid var(--accent)',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: '50%',
|
||||
};
|
||||
const handlePositionMap = {
|
||||
@@ -1610,27 +1719,32 @@ Organization : OptiHK Limited
|
||||
top: Position.Top,
|
||||
bottom: Position.Bottom
|
||||
};
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[data.ports, data.rotation, data.flip, data.flop]
|
||||
);
|
||||
const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }),
|
||||
[data.ports, data.rotation, data.flip, data.flop, componentSize]
|
||||
);
|
||||
const portDirectionMap = useMemo(
|
||||
() => new Map(portHandles.map(handle => [handle.name, handle.position])),
|
||||
[portHandles]
|
||||
);
|
||||
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 iconSize = createComponentSymbolMetrics(componentSize);
|
||||
const portLabelStyle = (portHandle) => {
|
||||
const base = { ...portHandle.style };
|
||||
if (portHandle.position === 'left') {
|
||||
return { ...base, right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
|
||||
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
|
||||
}
|
||||
if (portHandle.position === 'right') {
|
||||
return { ...base, left: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'left' };
|
||||
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: 'translateY(-50%)', textAlign: 'left' };
|
||||
}
|
||||
if (portHandle.position === 'top') {
|
||||
return { ...base, bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
}
|
||||
return { ...base, top: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1651,7 +1765,7 @@ Organization : OptiHK Limited
|
||||
height: visualSize.height,
|
||||
minHeight: visualSize.height,
|
||||
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
|
||||
transform: `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`,
|
||||
transform: componentVisualTransform,
|
||||
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
@@ -1684,22 +1798,34 @@ Organization : OptiHK Limited
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
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]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10 }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5 }}
|
||||
/>
|
||||
<React.Fragment key={`label-${portHandle.name}`}>
|
||||
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
||||
{portHandle.name}
|
||||
</span>
|
||||
@@ -1727,16 +1853,34 @@ Organization : OptiHK Limited
|
||||
|
||||
|
||||
// Renders standalone exported port elements with repeated port handles.
|
||||
const pinLabelFromPortName = (portName) => {
|
||||
const name = String(portName || '');
|
||||
const portMatch = name.match(/^port_(\d+)$/);
|
||||
if (portMatch) return portMatch[1];
|
||||
if (name === 'port') return '1';
|
||||
return name;
|
||||
};
|
||||
|
||||
const PortNode = ({ id, data, selected }) => {
|
||||
const angle = data.angle ?? 0;
|
||||
const canvasAngle = -Number(angle || 0);
|
||||
const portDisplayName = data.portName || data.componentDisplayName || data.label || 'port';
|
||||
const ports = buildElementPorts('port', data);
|
||||
const elementSize = buildElementBoxSize(data);
|
||||
const localHandlePorts = Object.fromEntries(
|
||||
Object.entries(ports).map(([name, info]) => [name, { ...info, a: 0 }])
|
||||
);
|
||||
const localPortHandles = useMemo(
|
||||
() => buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize }),
|
||||
[localHandlePorts, elementSize]
|
||||
);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(localHandlePorts, { rotation: 0 }),
|
||||
[localHandlePorts]
|
||||
() => buildPortHandles(localHandlePorts, { rotation: canvasAngle }),
|
||||
[localHandlePorts, canvasAngle]
|
||||
);
|
||||
const portDirectionMap = useMemo(
|
||||
() => new Map(portHandles.map(handle => [handle.name, handle.position])),
|
||||
[portHandles]
|
||||
);
|
||||
const handlePositionMap = {
|
||||
left: Position.Left,
|
||||
@@ -1746,27 +1890,61 @@ Organization : OptiHK Limited
|
||||
};
|
||||
const baseHandleStyle = {
|
||||
background: 'var(--accent)',
|
||||
width: 6,
|
||||
height: 6
|
||||
width: 5,
|
||||
height: 5
|
||||
};
|
||||
const pinLabelStyle = (portHandle) => {
|
||||
const base = {
|
||||
left: portHandle.style?.left,
|
||||
right: portHandle.style?.right,
|
||||
top: portHandle.style?.top,
|
||||
bottom: portHandle.style?.bottom
|
||||
};
|
||||
if (portHandle.position === 'left') return { ...base, transform: 'translate(calc(-100% - 5px), -50%)' };
|
||||
if (portHandle.position === 'right') return { ...base, transform: 'translate(5px, -50%)' };
|
||||
if (portHandle.position === 'top') return { ...base, transform: 'translate(-50%, calc(-100% - 5px))' };
|
||||
return { ...base, transform: 'translate(-50%, 5px)' };
|
||||
};
|
||||
const pinLabelTextStyle = {
|
||||
transform: `rotate(${-canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`
|
||||
};
|
||||
return (
|
||||
<div style={{
|
||||
width: elementSize.width, height: elementSize.height, borderRadius: 999,
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 8, fontWeight: 'bold',
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
}}>
|
||||
<span>P</span>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle type="source" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
|
||||
<Handle type="target" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div style={{ width: elementSize.width, height: elementSize.height, position: 'relative' }}>
|
||||
<div className="component-floating-label" title={portDisplayName}>
|
||||
<strong>{portDisplayName}</strong>
|
||||
<span>Port</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: elementSize.width, height: elementSize.height, borderRadius: 7,
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 8, fontWeight: 'bold',
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`,
|
||||
}}>
|
||||
{localPortHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style }}
|
||||
/>
|
||||
<div className="port-pin-label" style={pinLabelStyle(portHandle)}>
|
||||
<span style={pinLabelTextStyle}>{pinLabelFromPortName(portHandle.name)}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1776,17 +1954,18 @@ Organization : OptiHK Limited
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const anchorRotation = data.rotation || 0;
|
||||
const anchorVisualRotation = -Number(anchorRotation || 0);
|
||||
const anchorDisplayName = data.componentDisplayName || data.label || 'anchor';
|
||||
const ports = buildElementPorts('anchor', data);
|
||||
const elementSize = buildElementBoxSize(data);
|
||||
const localAnchorHandlePorts = Object.fromEntries(
|
||||
Object.entries(ports).map(([name, info]) => [name, { ...info, a: name.startsWith('a') || name.startsWith('left') ? 180 : 0 }])
|
||||
);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[localAnchorHandlePorts, data.flip, data.flop]
|
||||
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[localAnchorHandlePorts, elementSize, data.flip, data.flop]
|
||||
);
|
||||
const anchorDirectionHandles = useMemo(
|
||||
() => new Map(buildPortHandles(localAnchorHandlePorts, { rotation: Number(anchorRotation || 0), flip: Boolean(data.flip), flop: Boolean(data.flop) }).map(handle => [handle.name, handle.position])),
|
||||
() => new Map(buildPortHandles(localAnchorHandlePorts, { rotation: -Number(anchorRotation || 0), flip: Boolean(data.flip), flop: Boolean(data.flop) }).map(handle => [handle.name, handle.position])),
|
||||
[localAnchorHandlePorts, anchorRotation, data.flip, data.flop]
|
||||
);
|
||||
const handlePositionMap = {
|
||||
@@ -1796,8 +1975,8 @@ Organization : OptiHK Limited
|
||||
bottom: Position.Bottom
|
||||
};
|
||||
const baseHandleStyle = {
|
||||
width: 6,
|
||||
height: 6,
|
||||
width: 5,
|
||||
height: 5,
|
||||
background: 'var(--accent)',
|
||||
border: '1px solid var(--bg-main)',
|
||||
borderRadius: '50%'
|
||||
@@ -1806,18 +1985,10 @@ Organization : OptiHK Limited
|
||||
const name = String(portName || '');
|
||||
return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right';
|
||||
};
|
||||
const anchorPortVisualTop = (portName) => {
|
||||
const match = String(portName || '').match(/(\d+)$/);
|
||||
const index = match ? Math.max(1, Number(match[1])) : 1;
|
||||
const portCount = Math.max(1, Math.floor(Number(data.portNumber || 1)));
|
||||
if (portCount <= 1) return elementSize.height / 2;
|
||||
const travel = Math.max(0, elementSize.height - baseHandleStyle.height);
|
||||
return baseHandleStyle.height / 2 + ((index - 1) / (portCount - 1)) * travel;
|
||||
};
|
||||
const anchorHandleVisualStyle = (portHandle, zIndex) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = anchorPortVisualTop(portHandle.name);
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
...baseHandleStyle,
|
||||
zIndex,
|
||||
@@ -1828,47 +1999,66 @@ Organization : OptiHK Limited
|
||||
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',
|
||||
transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)'
|
||||
};
|
||||
};
|
||||
const pinLabelTextStyle = {
|
||||
transform: `rotate(${Number(anchorRotation || 0)}deg)`
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, data.ports, data.rotation, data.flip, data.flop, updateNodeInternals]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: elementSize.width,
|
||||
height: elementSize.height,
|
||||
borderRadius: 999,
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${anchorVisualRotation}deg)`,
|
||||
}}>
|
||||
<span>A</span>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 10)}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 5)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div className="anchor-node-shell" style={{ width: elementSize.width, height: elementSize.height }}>
|
||||
<div className="component-floating-label" title={anchorDisplayName}>
|
||||
<strong>{anchorDisplayName}</strong>
|
||||
<span>Anchor</span>
|
||||
</div>
|
||||
<div className="anchor-visual-body" style={{
|
||||
width: elementSize.width,
|
||||
height: elementSize.height,
|
||||
borderRadius: 999,
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${anchorVisualRotation}deg)`,
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 10)}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 5)}
|
||||
/>
|
||||
<div className="port-pin-label" style={pinLabelStyle(portHandle)}>
|
||||
<span style={pinLabelTextStyle}>{pinLabelFromPortName(portHandle.name)}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -2919,7 +3109,7 @@ Organization : OptiHK Limited
|
||||
onUpdateNode(selectedNode.id, {
|
||||
data: {
|
||||
basicArguments: nextArguments,
|
||||
ports: metadata?.ports || {},
|
||||
ports: metadata?.pins || metadata?.ports || {},
|
||||
boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize
|
||||
}
|
||||
});
|
||||
@@ -3558,6 +3748,10 @@ Organization : OptiHK Limited
|
||||
const [rulerStartPoint, setRulerStartPoint] = useState(null);
|
||||
const [rulerEndPoint, setRulerEndPoint] = useState(null);
|
||||
const [rulerPreviewPoint, setRulerPreviewPoint] = useState(null);
|
||||
const [mouseCanvasPoint, setMouseCanvasPoint] = useState(null);
|
||||
const [mouseScreenPoint, setMouseScreenPoint] = useState(null);
|
||||
const [canvasOrigin, setCanvasOrigin] = useState({ x: 0, y: 0 });
|
||||
const [originPickMode, setOriginPickMode] = useState(false);
|
||||
const [projectTechnology, setProjectTechnology] = useState('');
|
||||
const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||
const [currentLinkXsection, setCurrentLinkXsection] = useState('strip');
|
||||
@@ -3565,6 +3759,7 @@ Organization : OptiHK Limited
|
||||
const [clipboard, setClipboard] = useState({ nodes: [] });
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const canvasViewportRef = useRef(null);
|
||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||
|
||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||
@@ -3602,6 +3797,14 @@ Organization : OptiHK Limited
|
||||
[rulerStartPoint, rulerActiveEndPoint]
|
||||
);
|
||||
const rulerPreviewMeasurement = !rulerEndPoint && rulerPreviewPoint ? rulerMeasurement : null;
|
||||
const displayMousePoint = useMemo(() => (
|
||||
mouseCanvasPoint
|
||||
? {
|
||||
x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)),
|
||||
y: Number((mouseCanvasPoint.y - canvasOrigin.y).toFixed(3))
|
||||
}
|
||||
: null
|
||||
), [mouseCanvasPoint, canvasOrigin]);
|
||||
// Normalizes free-route control points and removes adjacent duplicates before storage.
|
||||
const compactRoutePoints = useCallback((points) => {
|
||||
return (points || [])
|
||||
@@ -3743,7 +3946,7 @@ Organization : OptiHK Limited
|
||||
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
|
||||
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
|
||||
const handles = buildPortHandles(buildElementPorts('anchor', node.data), {
|
||||
rotation: Number(node.data?.rotation || 0),
|
||||
rotation: -Number(node.data?.rotation || 0),
|
||||
flip: Boolean(node.data?.flip),
|
||||
flop: Boolean(node.data?.flop)
|
||||
});
|
||||
@@ -4414,10 +4617,7 @@ Organization : OptiHK Limited
|
||||
if (!element || typeof element !== 'object') return;
|
||||
const elementType = element.type === 'anchor' ? 'anchor' : (element.type === 'port' ? 'port' : '');
|
||||
if (!elementType) return;
|
||||
if (elementType === 'port' && elementName === 'port' && Array.isArray(doc.ports) && doc.ports.length > 0) {
|
||||
return;
|
||||
}
|
||||
const portNumberValue = Math.floor(Number(element.port_number ?? element.portNumber ?? 1));
|
||||
const portNumberValue = Math.floor(Number(element.pin_number ?? element.pinNumber ?? element.port_number ?? element.portNumber ?? 1));
|
||||
const portNumber = Number.isFinite(portNumberValue) ? Math.max(1, portNumberValue) : 1;
|
||||
const pitchValue = Number(element.pitch ?? DEFAULT_ELEMENT_PITCH);
|
||||
const pitch = Number.isFinite(pitchValue) ? Math.max(0, pitchValue) : DEFAULT_ELEMENT_PITCH;
|
||||
@@ -4436,6 +4636,7 @@ Organization : OptiHK Limited
|
||||
pitch,
|
||||
layer: element.layer || 'WG_CORE',
|
||||
description: element.description || '',
|
||||
pinNames: Object.fromEntries((element.pins || []).map(pin => [pin.role, pin.name]).filter(([role, name]) => role && name)),
|
||||
boxSize: buildElementBoxSize({ elementType, portNumber, pitch })
|
||||
};
|
||||
const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
@@ -4479,6 +4680,14 @@ Organization : OptiHK Limited
|
||||
return nodes;
|
||||
}, []);
|
||||
|
||||
const resolveLoadedPinHandle = useCallback((node, pinName) => {
|
||||
if (!node || !node.data?.elementType) return pinName;
|
||||
const elementType = node.data.elementType === 'anchor' ? 'anchor' : 'port';
|
||||
const ports = buildElementPorts(elementType, node.data);
|
||||
const matched = Object.keys(ports || {}).find(portName => getElementPinName(node, portName) === pinName);
|
||||
return matched || pinName;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const input = document.getElementById('open-yaml-input');
|
||||
if (!input) return;
|
||||
@@ -4547,7 +4756,7 @@ Organization : OptiHK Limited
|
||||
componentDisplayName: instName,
|
||||
type: isProject ? 'composite' : undefined,
|
||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
||||
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||
@@ -4569,13 +4778,17 @@ Organization : OptiHK Limited
|
||||
const sourceId = nodeNameMap[fromInst];
|
||||
const targetId = nodeNameMap[toInst];
|
||||
if (sourceId && targetId) {
|
||||
const sourceNode = newNodes.find(node => node.id === sourceId);
|
||||
const targetNode = newNodes.find(node => node.id === targetId);
|
||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||
const view = routeStyleForSettings(route, false);
|
||||
newEdges.push({
|
||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
sourceHandle: fromPort,
|
||||
targetHandle: toPort,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type: view.type,
|
||||
style: view.style,
|
||||
data: { route, points: routePoints },
|
||||
@@ -4591,8 +4804,8 @@ Organization : OptiHK Limited
|
||||
|
||||
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
||||
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
||||
const importedPort = Array.isArray(doc.ports) && doc.ports[0]
|
||||
? { x: Number(doc.ports[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.ports[0].y) : Number(doc.ports[0].y || 0), a: Number(doc.ports[0].angle ?? doc.ports[0].a ?? 0), width: Number(doc.ports[0].width || 0.5) }
|
||||
const importedPin = Array.isArray(doc.pins) && doc.pins[0]
|
||||
? { x: Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
|
||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||
const newPage = {
|
||||
id: newPageId,
|
||||
@@ -4603,8 +4816,8 @@ Organization : OptiHK Limited
|
||||
{
|
||||
id: 'page-port',
|
||||
type: 'portNode',
|
||||
position: { x: importedPort.x, y: importedPort.y },
|
||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPort.a, width: importedPort.width || 0.5, layer: 'WG_CORE', description: '' },
|
||||
position: { x: importedPin.x, y: importedPin.y },
|
||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPin.a, width: importedPin.width || 0.5, layer: 'WG_CORE', description: '' },
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
deletable: false,
|
||||
@@ -4612,7 +4825,7 @@ Organization : OptiHK Limited
|
||||
...newNodes,
|
||||
],
|
||||
edges: newEdges,
|
||||
port: importedPort,
|
||||
port: importedPin,
|
||||
};
|
||||
|
||||
setPages(prev => [...prev, newPage]);
|
||||
@@ -4663,7 +4876,7 @@ Organization : OptiHK Limited
|
||||
|
||||
input.addEventListener('change', handleFile);
|
||||
return () => input.removeEventListener('change', handleFile);
|
||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectCompositeMap(prev => {
|
||||
@@ -4713,9 +4926,9 @@ Organization : OptiHK Limited
|
||||
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
||||
const doc = jsyaml.load(content) || {};
|
||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
||||
const pagePort = firstPort
|
||||
? { x: Number(firstPort.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPort.y) : Number(firstPort.y || 0), a: Number(firstPort.angle ?? firstPort.a ?? 0), width: Number(firstPort.width || 0.5) }
|
||||
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
|
||||
const pagePort = firstPin
|
||||
? { x: Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
|
||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||
const nodeNameMap = {};
|
||||
const nodes = [
|
||||
@@ -4760,7 +4973,7 @@ Organization : OptiHK Limited
|
||||
componentDisplayName: instName,
|
||||
type: instIsComposite ? 'composite' : undefined,
|
||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
||||
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||
@@ -4781,13 +4994,17 @@ Organization : OptiHK Limited
|
||||
const sourceId = nodeNameMap[fromInst];
|
||||
const targetId = nodeNameMap[toInst];
|
||||
if (!sourceId || !targetId) return;
|
||||
const sourceNode = nodes.find(node => node.id === sourceId);
|
||||
const targetNode = nodes.find(node => node.id === targetId);
|
||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||
const view = routeStyleForSettings(route, false);
|
||||
edges.push({
|
||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
sourceHandle: fromPort,
|
||||
targetHandle: toPort,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type: view.type,
|
||||
style: view.style,
|
||||
data: { route, points: routePoints },
|
||||
@@ -4861,7 +5078,7 @@ Organization : OptiHK Limited
|
||||
};
|
||||
|
||||
loadProject();
|
||||
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
||||
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
||||
@@ -4949,7 +5166,7 @@ Organization : OptiHK Limited
|
||||
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, boxSize),
|
||||
data: {
|
||||
...node.data,
|
||||
ports: metadata.ports || {},
|
||||
ports: metadata.pins || metadata.ports || {},
|
||||
boxSize,
|
||||
foundry: metadata.foundry || '',
|
||||
process: metadata.process || ''
|
||||
@@ -5273,7 +5490,7 @@ Organization : OptiHK Limited
|
||||
type: 'composite',
|
||||
category: null,
|
||||
rotation: 0,
|
||||
ports: parsedData.ports || {},
|
||||
ports: parsedData.pins || parsedData.ports || {},
|
||||
boxSize: compositeBoxSize
|
||||
}
|
||||
};
|
||||
@@ -5324,7 +5541,7 @@ Organization : OptiHK Limited
|
||||
type: 'composite',
|
||||
category: null,
|
||||
rotation: 0,
|
||||
ports: parsedData.ports || {},
|
||||
ports: parsedData.pins || parsedData.ports || {},
|
||||
boxSize: compositeBoxSize
|
||||
}
|
||||
};
|
||||
@@ -5377,7 +5594,7 @@ Organization : OptiHK Limited
|
||||
libraryCategory: 'basic',
|
||||
category: 'basic',
|
||||
rotation: 0,
|
||||
ports: metadata?.ports || {},
|
||||
ports: metadata?.pins || metadata?.ports || {},
|
||||
boxSize: metadata ? normalizeBoxSize(metadata) : DEFAULT_COMPONENT_BOX_SIZE,
|
||||
basicArguments
|
||||
},
|
||||
@@ -5585,13 +5802,16 @@ Organization : OptiHK Limited
|
||||
setRulerStartPoint(null);
|
||||
setRulerEndPoint(null);
|
||||
setRulerPreviewPoint(null);
|
||||
} else {
|
||||
setOriginPickMode(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Convert a pane click or pointer event into canvas ruler coordinates.
|
||||
const eventToRulerPoint = useCallback((event) => {
|
||||
// Convert a pane click or pointer event into canvas coordinates.
|
||||
const eventToCanvasPoint = useCallback((event) => {
|
||||
if (!reactFlowInstance || !event) return null;
|
||||
const rawPoint = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
return {
|
||||
x: Number(Math.min(activeCanvasSize.width, Math.max(0, rawPoint.x)).toFixed(3)),
|
||||
@@ -5599,12 +5819,41 @@ Organization : OptiHK Limited
|
||||
};
|
||||
}, [reactFlowInstance, activeCanvasSize.width, activeCanvasSize.height]);
|
||||
|
||||
const updateMouseCanvasPoint = useCallback((event) => {
|
||||
if (!activePage || activePage.type === 'layoutPreview') return null;
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (!nextPoint) return null;
|
||||
setMouseCanvasPoint(nextPoint);
|
||||
const rect = canvasViewportRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
setMouseScreenPoint({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
});
|
||||
}
|
||||
return nextPoint;
|
||||
}, [activePage, eventToCanvasPoint]);
|
||||
|
||||
const toggleOriginPickMode = useCallback(() => {
|
||||
setOriginPickMode(prev => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setRulerMode(false);
|
||||
setRulerStartPoint(null);
|
||||
setRulerEndPoint(null);
|
||||
setRulerPreviewPoint(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Set ruler start/end points from canvas clicks.
|
||||
const handleRulerPaneClick = useCallback((event) => {
|
||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const nextPoint = eventToRulerPoint(event);
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (!nextPoint) return;
|
||||
if (!rulerStartPoint || rulerEndPoint) {
|
||||
setRulerStartPoint(nextPoint);
|
||||
setRulerEndPoint(null);
|
||||
@@ -5618,14 +5867,47 @@ Organization : OptiHK Limited
|
||||
if (measurement) {
|
||||
addLog(`Ruler distance: ${measurement.label}`);
|
||||
}
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint, addLog]);
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint, addLog]);
|
||||
|
||||
// Update the live ruler preview point while measuring.
|
||||
const handleRulerMouseMove = useCallback((event) => {
|
||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||
if (!rulerStartPoint || rulerEndPoint) return;
|
||||
setRulerPreviewPoint(eventToRulerPoint(event));
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint]);
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (nextPoint) setRulerPreviewPoint(nextPoint);
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint]);
|
||||
|
||||
const chooseCanvasOriginFromEvent = useCallback((event) => {
|
||||
if (!originPickMode || !activePage || activePage.type === 'layoutPreview') return false;
|
||||
const nextPoint = updateMouseCanvasPoint(event) || eventToCanvasPoint(event);
|
||||
if (!nextPoint) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setCanvasOrigin(nextPoint);
|
||||
setOriginPickMode(false);
|
||||
addLog(`Canvas origin: (${nextPoint.x.toFixed(3)}, ${nextPoint.y.toFixed(3)}) um`);
|
||||
return true;
|
||||
}, [originPickMode, activePage, updateMouseCanvasPoint, eventToCanvasPoint, addLog]);
|
||||
|
||||
const handleCanvasMouseMove = useCallback((event) => {
|
||||
updateMouseCanvasPoint(event);
|
||||
handleRulerMouseMove(event);
|
||||
}, [updateMouseCanvasPoint, handleRulerMouseMove]);
|
||||
|
||||
const handleCanvasPaneClick = useCallback((event) => {
|
||||
if (chooseCanvasOriginFromEvent(event)) return;
|
||||
handleRulerPaneClick(event);
|
||||
}, [chooseCanvasOriginFromEvent, handleRulerPaneClick]);
|
||||
|
||||
const handleCanvasNodeClick = useCallback((event) => {
|
||||
if (chooseCanvasOriginFromEvent(event)) return;
|
||||
handleRulerPaneClick(event);
|
||||
}, [chooseCanvasOriginFromEvent, handleRulerPaneClick]);
|
||||
|
||||
const handleCanvasMouseLeave = useCallback(() => {
|
||||
setMouseCanvasPoint(null);
|
||||
setMouseScreenPoint(null);
|
||||
}, []);
|
||||
|
||||
// Select a route edge by id with optional additive selection.
|
||||
const selectEdgeById = useCallback((edgeId, additive = false) => {
|
||||
@@ -5873,7 +6155,7 @@ type: ${page.type === 'project' ? 'project' : 'composite'}
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
${buildCanvasPortsYaml(page.nodes)}
|
||||
${buildCanvasPinsYaml(page.nodes)}
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:`;
|
||||
@@ -6098,8 +6380,11 @@ ${bundlesBlock}`;
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
ref={canvasViewportRef}
|
||||
style={{ flex: 1, position: 'relative' }}
|
||||
onMouseDownCapture={handleCanvasMouseDown}
|
||||
onMouseMoveCapture={handleCanvasMouseMove}
|
||||
onMouseLeave={handleCanvasMouseLeave}
|
||||
>
|
||||
<div className="canvas-toolbar">
|
||||
<span className="grid-snap-label">Snap to Grid</span>
|
||||
@@ -6154,6 +6439,14 @@ ${bundlesBlock}`;
|
||||
<button className="mini-btn" onClick={toggleRulerMode} aria-pressed={rulerMode ? 'true' : 'false'}>
|
||||
{rulerMode ? 'Ruler On' : 'Ruler'}
|
||||
</button>
|
||||
<button
|
||||
className={`mini-btn origin-select-btn ${originPickMode ? 'active' : ''}`}
|
||||
onClick={toggleOriginPickMode}
|
||||
aria-pressed={originPickMode ? 'true' : 'false'}
|
||||
title="Select canvas origin"
|
||||
>
|
||||
{originPickMode ? 'Picking Origin' : 'Origin Select'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{buildProgress.active && (
|
||||
@@ -6176,6 +6469,25 @@ ${bundlesBlock}`;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activePage && activePage.type !== 'layoutPreview' && (
|
||||
<div
|
||||
className="coordinate-readout"
|
||||
style={{ bottom: rulerMode ? 58 : 18 }}
|
||||
title={`Origin (${canvasOrigin.x.toFixed(3)}, ${canvasOrigin.y.toFixed(3)}) um`}
|
||||
>
|
||||
X {displayMousePoint ? displayMousePoint.x.toFixed(3) : '--'} um
|
||||
Y {displayMousePoint ? displayMousePoint.y.toFixed(3) : '--'} um
|
||||
<span>O {canvasOrigin.x.toFixed(3)}, {canvasOrigin.y.toFixed(3)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{originPickMode && mouseScreenPoint && activePage?.type !== 'layoutPreview' && (
|
||||
<div
|
||||
className="origin-crosshair"
|
||||
style={{ left: mouseScreenPoint.x, top: mouseScreenPoint.y }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activePage && activePage.type !== 'layoutPreview' && (
|
||||
<button
|
||||
onClick={handleBuildLayout}
|
||||
@@ -6196,12 +6508,12 @@ ${bundlesBlock}`;
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={handleBasicConnection}
|
||||
onPaneClick={handleRulerPaneClick}
|
||||
onPaneMouseMove={handleRulerMouseMove}
|
||||
onPaneClick={handleCanvasPaneClick}
|
||||
onPaneMouseMove={handleCanvasMouseMove}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onNodeClick={handleRulerPaneClick}
|
||||
onNodeMouseMove={handleRulerMouseMove}
|
||||
onNodeClick={handleCanvasNodeClick}
|
||||
onNodeMouseMove={handleCanvasMouseMove}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeMouseDown={onNodeMouseDown}
|
||||
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
||||
|
||||
@@ -16,8 +16,8 @@ assert(
|
||||
'canvas.html should use buildInstancesYaml for layout instance export'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('buildCanvasPortsYaml(page.nodes)'),
|
||||
'canvas.html should export ports from active canvas port nodes'
|
||||
canvasHtml.includes('buildCanvasPinsYaml(page.nodes)'),
|
||||
'canvas.html should export pins from active canvas port nodes'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('buildPageComponentPorts(page.port, page.nodes)'),
|
||||
|
||||
+147
-14
@@ -24,8 +24,12 @@ assert.deepStrictEqual(handles.filter(handle => handle.position === 'right').map
|
||||
assert.deepStrictEqual(handles.find(handle => handle.name === 'ep2b').position, 'top');
|
||||
assert.deepStrictEqual(handles.find(handle => handle.name === 'ep2a').position, 'bottom');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'a1').style.top, '15%');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'a1').style.left, 0);
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'a2').style.top, '85%');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'b1').style.left, '100%');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'ep2b').style.left, '50%');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'ep2b').style.top, 0);
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'ep2a').style.top, '100%');
|
||||
|
||||
const uniformLeftHandles = helpers.buildPortHandles({
|
||||
p_top: { x: -10, y: 300, a: 180 },
|
||||
@@ -37,6 +41,46 @@ assert.deepStrictEqual(
|
||||
['15%', '50%', '85%'],
|
||||
'ports on the same side should be uniformly spaced after sorting'
|
||||
);
|
||||
const exactCenteredHandles = helpers.buildPortHandles({
|
||||
a1: { x: -91.7, y: 4.475, a: 180 },
|
||||
a2: { x: -91.7, y: -4.475, a: 180 },
|
||||
b1: { x: 91.7, y: 4.475, a: 0 },
|
||||
b2: { x: 91.7, y: -4.475, a: 0 },
|
||||
}, { boxSize: { width: 183.4, height: 29.65 } });
|
||||
assert.strictEqual(
|
||||
exactCenteredHandles.find(handle => handle.name === 'a1').style.top,
|
||||
'34.907%',
|
||||
'box_size-aware handles should use exact centered PDK y coordinates instead of uniform spacing'
|
||||
);
|
||||
assert.strictEqual(exactCenteredHandles.find(handle => handle.name === 'a2').style.top, '65.093%');
|
||||
assert.strictEqual(exactCenteredHandles.find(handle => handle.name === 'b1').style.left, '100%');
|
||||
const exactPositiveHandles = helpers.buildPortHandles({
|
||||
a1: { x: 0, y: 10, a: 180 },
|
||||
b1: { x: 150.4, y: 10, a: 0 },
|
||||
a2: { x: 0, y: -10, a: 180 },
|
||||
b2: { x: 150.4, y: -10, a: 0 },
|
||||
}, { boxSize: { width: 150.4, height: 40 } });
|
||||
assert.strictEqual(exactPositiveHandles.find(handle => handle.name === 'a1').style.top, '25%');
|
||||
assert.strictEqual(exactPositiveHandles.find(handle => handle.name === 'a2').style.top, '75%');
|
||||
const exactPortObjectHandles = helpers.buildPortHandles({
|
||||
port_1: { x: 0, y: 10, a: 0 },
|
||||
port_2: { x: 0, y: 0, a: 0 },
|
||||
port_3: { x: 0, y: -10, a: 0 },
|
||||
}, { boxSize: { width: 47, height: 58 } });
|
||||
assert.deepStrictEqual(
|
||||
exactPortObjectHandles.map(handle => handle.style.top),
|
||||
['32.759%', '50%', '67.241%'],
|
||||
'standalone Port handles should use their actual y offsets inside the Port body'
|
||||
);
|
||||
const exactAnchorHandles = helpers.buildPortHandles(
|
||||
helpers.buildElementPorts('anchor', { portNumber: 3, pitch: 10 }),
|
||||
{ boxSize: { width: 16, height: 58 } }
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
exactAnchorHandles.filter(handle => handle.name.startsWith('a')).map(handle => handle.style.top),
|
||||
['32.759%', '50%', '67.241%'],
|
||||
'standalone Anchor handles should be centered around the Anchor body'
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
helpers.normalizeBoxSize({ box_size: [946, 75] }),
|
||||
@@ -168,6 +212,34 @@ assert.strictEqual(
|
||||
'bottom',
|
||||
'rotating a component should rotate vertical port handle sides'
|
||||
);
|
||||
const quarterTurnHandles = helpers.buildPortHandles({
|
||||
out: { x: 50, y: 0, a: 0 },
|
||||
up: { x: 0, y: 20, a: 90 },
|
||||
}, { rotation: 90 });
|
||||
assert.strictEqual(
|
||||
quarterTurnHandles.find(handle => handle.name === 'out').position,
|
||||
'bottom',
|
||||
'90 degree canvas rotation should move a right-facing port direction to the bottom side'
|
||||
);
|
||||
assert.strictEqual(
|
||||
quarterTurnHandles.find(handle => handle.name === 'up').position,
|
||||
'right',
|
||||
'90 degree canvas rotation should move a top-facing port direction to the right side'
|
||||
);
|
||||
const negativeQuarterTurnHandles = helpers.buildPortHandles({
|
||||
out: { x: 50, y: 0, a: 0 },
|
||||
down: { x: 0, y: -20, a: -90 },
|
||||
}, { rotation: -90 });
|
||||
assert.strictEqual(
|
||||
negativeQuarterTurnHandles.find(handle => handle.name === 'out').position,
|
||||
'top',
|
||||
'-90 degree canvas rotation should move a right-facing port direction to the top side'
|
||||
);
|
||||
assert.strictEqual(
|
||||
negativeQuarterTurnHandles.find(handle => handle.name === 'down').position,
|
||||
'right',
|
||||
'-90 degree canvas rotation should move a bottom-facing port direction to the right side'
|
||||
);
|
||||
|
||||
const args = helpers.createForgeArguments();
|
||||
assert(Object.keys(args).length >= 10);
|
||||
@@ -230,8 +302,10 @@ assert.deepStrictEqual(
|
||||
const ninetyBendHandles = helpers.buildPortHandles(helpers.buildBasicComponentPorts('90 bend', { radius: 15 }));
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').position, 'left');
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').style.top, '50%');
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').style.left, 0);
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').position, 'top');
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.left, '50%');
|
||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.top, 0);
|
||||
assert.deepStrictEqual(
|
||||
helpers.getBasicComponentMetadata('180 bend', { radius: 15 }).box_size,
|
||||
[25, 50],
|
||||
@@ -318,14 +392,17 @@ assert(projectInstancesYaml.includes('cell_1:'));
|
||||
assert(projectInstancesYaml.includes('component: canvas_1'));
|
||||
|
||||
const pagePortsYaml = helpers.buildPortsYaml({ x: 50, y: 150, a: 90 });
|
||||
assert(pagePortsYaml.includes('- name: port'));
|
||||
assert(pagePortsYaml.includes('pins:'));
|
||||
assert(!pagePortsYaml.includes('ports:'));
|
||||
assert(pagePortsYaml.includes('- name: port_io1'));
|
||||
assert(pagePortsYaml.includes('pin: io1'));
|
||||
assert(pagePortsYaml.includes('x: 50.0'));
|
||||
assert(pagePortsYaml.includes('y: -150.0'));
|
||||
assert(pagePortsYaml.includes('angle: -90.0'));
|
||||
|
||||
const componentPorts = helpers.buildPageComponentPorts({ x: 12, y: -6, a: 180 });
|
||||
assert.deepStrictEqual(componentPorts, {
|
||||
port: { x: 12, y: -6, a: 0, width: 0.5 }
|
||||
port_io1: { element: 'port', pin: 'io1', x: 12, y: -6, a: 0, width: 0.5 }
|
||||
});
|
||||
|
||||
const elementNodes = [
|
||||
@@ -373,8 +450,9 @@ assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('port', { portNumbe
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('port', { portNumber: 3, pitch: 10 }).port_1, { x: 0, y: 10, a: 0, width: 0.5 });
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('port', { portNumber: 3 }).port_1, { x: 0, y: 10, a: 0, width: 0.5 });
|
||||
assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 })), ['a1', 'b1', 'a2', 'b2']);
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).b2, { x: 0, y: -21, a: 0, width: 0.5 });
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).a2, { x: 0, y: -21, a: 180, width: 0.5 });
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).a1, { x: 0, y: 6, a: 180, width: 0.5 });
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).b2, { x: 0, y: -6, a: 0, width: 0.5 });
|
||||
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).a2, { x: 0, y: -6, a: 180, width: 0.5 });
|
||||
assert.deepStrictEqual(
|
||||
helpers.getNodePortCanvasPoint({
|
||||
id: 'anchor-rotated',
|
||||
@@ -391,10 +469,31 @@ assert.deepStrictEqual(
|
||||
{ x: 115, y: 200 },
|
||||
'Anchor port endpoint coordinates should rotate with the anchor body'
|
||||
);
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 1 }), { width: 30, height: 30 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 1 }), { width: 8, height: 30 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 4, pitch: 10 }), { width: 8, height: 72 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 4, pitch: 10 }), { width: 30, height: 72 });
|
||||
assert.deepStrictEqual(
|
||||
helpers.getNodePortCanvasPoint({
|
||||
id: 'port-rotated',
|
||||
type: 'portNode',
|
||||
position: { x: 100, y: 200 },
|
||||
data: {
|
||||
elementType: 'port',
|
||||
angle: 180,
|
||||
portNumber: 3,
|
||||
pitch: 10,
|
||||
ports: helpers.buildElementPorts('port', { angle: 180, portNumber: 3, pitch: 10 })
|
||||
}
|
||||
}, 'port_1'),
|
||||
{ x: 100, y: 210 },
|
||||
'Port pin endpoint coordinates should rotate with the Port body'
|
||||
);
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 1 }), { width: 47, height: 30 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 1 }), { width: 16, height: 30 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 4, pitch: 10 }), { width: 16, height: 72 });
|
||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 4, pitch: 10 }), { width: 47, height: 72 });
|
||||
assert.deepStrictEqual(
|
||||
helpers.buildElementBoxSize({ elementType: 'port', portName: 'input_port' }),
|
||||
{ width: 82, height: 30 },
|
||||
'Port object width should grow to fit the displayed port instance name'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
helpers.buildPageComponentPorts(null, [{
|
||||
id: 'port-array',
|
||||
@@ -403,14 +502,44 @@ assert.deepStrictEqual(
|
||||
data: { componentDisplayName: 'array', elementType: 'port', portNumber: 3, pitch: 10, width: 0.6 }
|
||||
}]),
|
||||
{
|
||||
array_1: { x: 100, y: 190, a: 180, width: 0.6 },
|
||||
array_2: { x: 100, y: 200, a: 180, width: 0.6 },
|
||||
array_3: { x: 100, y: 210, a: 180, width: 0.6 }
|
||||
array_io1: { element: 'array', pin: 'io1', x: 100, y: 190, a: 180, width: 0.6 },
|
||||
array_io2: { element: 'array', pin: 'io2', x: 100, y: 200, a: 180, width: 0.6 },
|
||||
array_io3: { element: 'array', pin: 'io3', x: 100, y: 210, a: 180, width: 0.6 }
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
helpers.buildPageComponentPorts(null, [{
|
||||
id: 'port-array-rotated-90',
|
||||
type: 'portNode',
|
||||
position: { x: 100, y: 200 },
|
||||
data: { componentDisplayName: 'array', elementType: 'port', angle: 90, portNumber: 2, pitch: 10, width: 0.6 }
|
||||
}]),
|
||||
{
|
||||
array_io1: { element: 'array', pin: 'io1', x: 95, y: 200, a: -90, width: 0.6 },
|
||||
array_io2: { element: 'array', pin: 'io2', x: 105, y: 200, a: -90, width: 0.6 }
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
helpers.buildPageComponentPorts(null, [{
|
||||
id: 'port-array-rotated',
|
||||
type: 'portNode',
|
||||
position: { x: 100, y: 200 },
|
||||
data: { componentDisplayName: 'array', elementType: 'port', angle: 180, portNumber: 3, pitch: 10, width: 0.6 }
|
||||
}]),
|
||||
{
|
||||
array_io1: { element: 'array', pin: 'io1', x: 100, y: 210, a: 0, width: 0.6 },
|
||||
array_io2: { element: 'array', pin: 'io2', x: 100, y: 200, a: 0, width: 0.6 },
|
||||
array_io3: { element: 'array', pin: 'io3', x: 100, y: 190, a: 0, width: 0.6 }
|
||||
},
|
||||
'Rotated Port object pins should export rotated coordinates along with their rotated angle'
|
||||
);
|
||||
|
||||
const canvasPortsYaml = helpers.buildCanvasPortsYaml(elementNodes);
|
||||
assert(canvasPortsYaml.includes('name: in0'));
|
||||
assert(canvasPortsYaml.includes('pins:'));
|
||||
assert(!canvasPortsYaml.includes('ports:'));
|
||||
assert(canvasPortsYaml.includes('name: in0_io1'));
|
||||
assert(canvasPortsYaml.includes('element: in0'));
|
||||
assert(canvasPortsYaml.includes('pin: io1'));
|
||||
assert(canvasPortsYaml.includes('description: "input port"'));
|
||||
assert(canvasPortsYaml.includes('width: 0.7'));
|
||||
assert(canvasPortsYaml.includes('y: -20.0'));
|
||||
@@ -422,7 +551,11 @@ assert(elementsYaml.includes('type: port'));
|
||||
assert(elementsYaml.includes('anchor_1:'));
|
||||
assert(elementsYaml.includes('type: anchor'));
|
||||
assert(elementsYaml.includes('y: -20.0'));
|
||||
assert(elementsYaml.includes('port_number: 1'));
|
||||
assert(elementsYaml.includes('pin_number: 1'));
|
||||
assert(elementsYaml.includes('name: in0_io1'));
|
||||
assert(elementsYaml.includes('role: io1'));
|
||||
assert(elementsYaml.includes('name: anchor_1_a1'));
|
||||
assert(elementsYaml.includes('role: a1'));
|
||||
assert(elementsYaml.includes('pitch: 10'));
|
||||
|
||||
const instancesWithoutElements = helpers.buildInstancesYaml({
|
||||
@@ -435,7 +568,7 @@ assert(instancesWithoutElements.includes('component_1:'));
|
||||
assert(instancesWithoutElements.includes('y: -60.0'));
|
||||
|
||||
const multiPortComponentPorts = helpers.buildPageComponentPorts(null, elementNodes);
|
||||
assert.deepStrictEqual(multiPortComponentPorts.in0, { x: 10, y: 20, a: 0, width: 0.7 });
|
||||
assert.deepStrictEqual(multiPortComponentPorts.in0_io1, { element: 'in0', pin: 'io1', x: 10, y: 20, a: 0, width: 0.7 });
|
||||
|
||||
const technologyManifest = {
|
||||
defaults: { xsection: 'strip', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
||||
|
||||
@@ -90,22 +90,41 @@ assert(
|
||||
gdsBuilderPy.includes('_cells_have_links') && gdsBuilderPy.includes('Routed Build GDS requires mxpic_router'),
|
||||
'Build GDS should not silently fall back to unrouted gdstk when links are present'
|
||||
);
|
||||
assert(
|
||||
gdsBuilderPy.includes('_cells_have_elements') &&
|
||||
gdsBuilderPy.includes('Build GDS with Port/Anchor elements requires Nazca') &&
|
||||
gdsBuilderPy.indexOf('if _cells_have_elements(cells):') < gdsBuilderPy.indexOf('return _build_with_gdstk'),
|
||||
'Build GDS should route layouts with Port/Anchor elements through Nazca so element pins survive as cell metadata'
|
||||
);
|
||||
assert(
|
||||
gdsBuilderPy.includes('_build_nazca_element_cells') &&
|
||||
gdsBuilderPy.includes('_build_nazca_element_cell') &&
|
||||
gdsBuilderPy.includes('element.get("pin_number"') &&
|
||||
gdsBuilderPy.includes('_element_pin_names') &&
|
||||
gdsBuilderPy.includes('nd.Pin(pin_name, width=width).put(0.0, y, 180.0)') &&
|
||||
gdsBuilderPy.includes('nd.Pin(anchor_pin_names[index * 2], width=width).put(0.0, y, 180.0)') &&
|
||||
gdsBuilderPy.includes('nd.Pin(anchor_pin_names[index * 2 + 1], width=width).put(0.0, y, 0.0)') &&
|
||||
gdsBuilderPy.includes('element_cell.put(x, y, rotation)'),
|
||||
'Nazca fallback should model Port and Anchor objects as placed element cells with named local nd.Pin definitions'
|
||||
);
|
||||
|
||||
const routerDir = path.resolve(root, '..', 'mxpic_router', 'mxpic_router');
|
||||
if (fs.existsSync(routerDir)) {
|
||||
const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8');
|
||||
const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8');
|
||||
assert(
|
||||
routerLoaderPy.includes('port_number: int = 1') &&
|
||||
routerLoaderPy.includes('pins: Dict[str, PinSpec]') &&
|
||||
routerLoaderPy.includes('pin_number') &&
|
||||
routerLoaderPy.includes('pitch: float = 10.0') &&
|
||||
routerLoaderPy.includes('port_number=_int(element.get("port_number"'),
|
||||
'mxpic_router loader should parse multi-port anchor metadata from exported elements'
|
||||
routerLoaderPy.includes('pins=_pins(element.get("pins"))'),
|
||||
'mxpic_router loader should parse pins-only layout metadata from exported elements'
|
||||
);
|
||||
assert(
|
||||
routerBuilderPy.includes('for index in range(port_number):') &&
|
||||
routerBuilderPy.includes('a{index + 1}') &&
|
||||
routerBuilderPy.includes('b{index + 1}'),
|
||||
'mxpic_router builder should register aN/bN pins for multi-port anchors'
|
||||
routerBuilderPy.includes('_port_element_pin_entries') &&
|
||||
routerBuilderPy.includes('_anchor_element_pin_entries') &&
|
||||
routerBuilderPy.includes('_metadata_pins') &&
|
||||
routerBuilderPy.includes('link.src_pin'),
|
||||
'mxpic_router builder should register named element pins and route through pin endpoints'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -213,21 +213,20 @@ assert(
|
||||
canvasHtml.includes('const anchorRotation = data.rotation || 0') &&
|
||||
canvasHtml.includes('const anchorVisualRotation = -Number(anchorRotation || 0)') &&
|
||||
canvasHtml.includes('transform: `rotate(${anchorVisualRotation}deg)`') &&
|
||||
canvasHtml.includes('buildPortHandles(localAnchorHandlePorts, { rotation: 0') &&
|
||||
canvasHtml.includes('buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize') &&
|
||||
canvasHtml.includes('anchorDirectionHandles') &&
|
||||
canvasHtml.includes('rotation: Number(anchorRotation || 0)') &&
|
||||
canvasHtml.includes('rotation: -Number(anchorRotation || 0)') &&
|
||||
canvasHtml.includes('anchorHandleVisualStyle(portHandle') &&
|
||||
canvasHtml.includes('anchorPortVisualSide') &&
|
||||
canvasHtml.includes('portHandle.name') &&
|
||||
canvasHtml.includes('visualSide === \'left\' ? 0 : elementSize.width') &&
|
||||
canvasHtml.includes('anchorPortVisualTop') &&
|
||||
canvasHtml.includes('(index - 1) / (portCount - 1)') &&
|
||||
canvasHtml.includes('elementSize.height - baseHandleStyle.height') &&
|
||||
canvasHtml.includes('transform: \'translate(-50%, -50%)\'') &&
|
||||
canvasHtml.includes("portHandle.style?.top || '50%'") &&
|
||||
canvasHtml.includes('localLeft') &&
|
||||
canvasHtml.includes('localTop') &&
|
||||
canvasHtml.includes('handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]') &&
|
||||
canvasHtml.includes('getAnchorHandleRouteDirection') &&
|
||||
canvasHtml.includes('rotation: Number(node.data?.rotation || 0)') &&
|
||||
canvasHtml.includes('rotation: -Number(node.data?.rotation || 0)') &&
|
||||
canvasHtml.includes('directionToReactFlowPosition') &&
|
||||
canvasHtml.includes('sourcePosition: directionToReactFlowPosition(sourceDirection)') &&
|
||||
canvasHtml.includes('targetPosition: directionToReactFlowPosition(targetDirection)') &&
|
||||
@@ -282,6 +281,16 @@ assert(
|
||||
canvasHtml.includes('component-floating-label') && canvasHtml.includes('component-visual-body'),
|
||||
'component labels should float outside the rotated body'
|
||||
);
|
||||
assert(
|
||||
!canvasHtml.includes('const visualPortHandles = useMemo(') &&
|
||||
canvasHtml.includes('buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize })') &&
|
||||
canvasHtml.includes('const portDirectionMap = useMemo(') &&
|
||||
canvasHtml.includes('position: \'absolute\', inset: 0') &&
|
||||
canvasHtml.includes('pointerEvents: \'none\'') &&
|
||||
canvasHtml.includes('pointerEvents: \'all\'') &&
|
||||
canvasHtml.includes('handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]'),
|
||||
'component port circles should use transformed pin positions so rendered sides follow pin angles'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('canvasTextVisible') &&
|
||||
canvasHtml.includes('toggleCanvasText') &&
|
||||
@@ -353,11 +362,26 @@ assert(
|
||||
assert(
|
||||
canvasHtml.includes('Ruler') &&
|
||||
canvasHtml.includes('rulerMode') &&
|
||||
canvasHtml.includes('onPaneClick={handleRulerPaneClick}') &&
|
||||
canvasHtml.includes('onNodeClick={handleRulerPaneClick}') &&
|
||||
canvasHtml.includes('onPaneMouseMove={handleRulerMouseMove}'),
|
||||
canvasHtml.includes('onPaneClick={handleCanvasPaneClick}') &&
|
||||
canvasHtml.includes('onNodeClick={handleCanvasNodeClick}') &&
|
||||
canvasHtml.includes('onPaneMouseMove={handleCanvasMouseMove}'),
|
||||
'canvas should expose a ruler mode controlled from the top toolbar, allow measuring on component bodies, and preview to the mouse'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('mouseCanvasPoint') &&
|
||||
canvasHtml.includes('canvasOrigin') &&
|
||||
canvasHtml.includes('originPickMode') &&
|
||||
canvasHtml.includes('displayMousePoint') &&
|
||||
canvasHtml.includes('toggleOriginPickMode') &&
|
||||
canvasHtml.includes('onMouseMoveCapture={handleCanvasMouseMove}') &&
|
||||
canvasHtml.includes('handleCanvasPaneClick') &&
|
||||
canvasHtml.includes('handleCanvasNodeClick') &&
|
||||
canvasHtml.includes('origin-select-btn') &&
|
||||
canvasHtml.includes('Select canvas origin') &&
|
||||
canvasHtml.includes('className="coordinate-readout"') &&
|
||||
canvasHtml.includes('className="origin-crosshair"'),
|
||||
'canvas should show live mouse coordinates and support one-click origin selection with a crosshair preview'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('createRulerMeasurement') &&
|
||||
canvasHtml.includes('rulerPointNode') &&
|
||||
@@ -442,11 +466,42 @@ assert(
|
||||
canvasHtml.includes('font-size: 0.4rem;') &&
|
||||
canvasHtml.includes('font-size: 0.32rem;') &&
|
||||
canvasHtml.includes("font: 600 0.5rem/1.35") &&
|
||||
canvasHtml.includes('width: 8, height: 8') &&
|
||||
canvasHtml.includes('width: 6,') &&
|
||||
canvasHtml.includes('width: 6, height: 6') &&
|
||||
canvasHtml.includes("border: '1px solid var(--accent)'") &&
|
||||
canvasHtml.includes('width: 5,') &&
|
||||
canvasHtml.includes('fontSize: 8'),
|
||||
'canvas labels and port circles should render smaller than the previous sizing'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('const portDisplayName = data.portName || data.componentDisplayName || data.label || \'port\';') &&
|
||||
canvasHtml.includes('const canvasAngle = -Number(angle || 0);') &&
|
||||
canvasHtml.includes('const pinLabelFromPortName =') &&
|
||||
canvasHtml.includes('buildPortHandles(localHandlePorts, { rotation: canvasAngle })') &&
|
||||
canvasHtml.includes('buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize })') &&
|
||||
canvasHtml.includes('style={{ ...baseHandleStyle, ...portHandle.style }}') &&
|
||||
canvasHtml.includes('borderRadius: 7') &&
|
||||
canvasHtml.includes('boxSizing: \'border-box\'') &&
|
||||
canvasHtml.includes('width: elementSize.width, height: elementSize.height, position: \'relative\'') &&
|
||||
canvasHtml.includes('className="component-floating-label"') &&
|
||||
canvasHtml.includes('className="port-pin-label"') &&
|
||||
canvasHtml.includes('pinLabelFromPortName(portHandle.name)') &&
|
||||
canvasHtml.includes('pinLabelStyle(portHandle') &&
|
||||
canvasHtml.includes('transform: `rotate(${canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`') &&
|
||||
canvasHtml.includes('{portDisplayName}'),
|
||||
'standalone Port nodes should render pin labels at their circles while the port instance name floats outside the rotated body'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('const anchorDisplayName = data.componentDisplayName || data.label || \'anchor\';') &&
|
||||
canvasHtml.includes('className="anchor-node-shell"') &&
|
||||
canvasHtml.includes('className="anchor-visual-body"') &&
|
||||
canvasHtml.includes("const localLeft = visualSide === 'left' ? 0 : elementSize.width") &&
|
||||
canvasHtml.includes("transform: 'translate(-50%, -50%)'") &&
|
||||
canvasHtml.includes('pinLabelFromPortName(portHandle.name)') &&
|
||||
canvasHtml.includes('pinLabelStyle(portHandle') &&
|
||||
canvasHtml.includes('className="port-pin-label"') &&
|
||||
canvasHtml.includes('{anchorDisplayName}'),
|
||||
'standalone Anchor nodes should use the same outside name label and per-pin labels as Port nodes'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('ParallelRouteEdge') &&
|
||||
canvasHtml.includes('parallelOffset') &&
|
||||
|
||||
Reference in New Issue
Block a user