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.
|
# 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
|
# Developer : Qin Yue @ 2026
|
||||||
# Organization : OptiHK Limited
|
# Organization : OptiHK Limited
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -58,7 +58,19 @@ def build_project_gds(
|
|||||||
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
|
registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds)
|
||||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
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.
|
# environments where gdstk is not installed.
|
||||||
try:
|
try:
|
||||||
return _build_with_gdstk(cells, output_path, registry)
|
return _build_with_gdstk(cells, output_path, registry)
|
||||||
@@ -132,6 +144,16 @@ def _cells_have_links(cells: Dict[str, dict]) -> bool:
|
|||||||
return False
|
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:
|
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."""
|
"""Assemble a project GDS with gdstk when Nazca or routed building is unavailable."""
|
||||||
import gdstk
|
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.
|
# is exported as the top-level GDS.
|
||||||
for cell_name in ordered_names:
|
for cell_name in ordered_names:
|
||||||
data = cells[cell_name]
|
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:
|
with nd.Cell(cell_name) as current_cell:
|
||||||
for instance_name, instance in (data.get("instances") or {}).items():
|
for instance_name, instance in (data.get("instances") or {}).items():
|
||||||
component = str(instance.get("component") or "")
|
component = str(instance.get("component") or "")
|
||||||
@@ -211,6 +234,12 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg
|
|||||||
continue
|
continue
|
||||||
loaded = nd.load_gds(asset.gds_path)
|
loaded = nd.load_gds(asset.gds_path)
|
||||||
loaded.put(x, y, rotation)
|
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
|
built_cells[cell_name] = current_cell
|
||||||
|
|
||||||
top_name = ordered_names[-1]
|
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)
|
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:
|
def _safe_cell_name(name: str, existing: dict) -> str:
|
||||||
"""Generate a backend-safe unique cell name for GDS/Nazca libraries."""
|
"""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"
|
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
|
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:
|
def _number(value, default=0.0) -> float:
|
||||||
"""Convert numeric YAML values to floats with a stable default."""
|
"""Convert numeric YAML values to floats with a stable default."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
+18
-1
@@ -107,7 +107,24 @@ class PdkRegistry:
|
|||||||
if not yaml_path:
|
if not yaml_path:
|
||||||
return None
|
return None
|
||||||
with open(yaml_path, "r", encoding="utf-8") as file:
|
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:
|
def _inside_root(self, path: str) -> bool:
|
||||||
"""Check that a candidate asset path remains inside the permitted PDK root."""
|
"""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:
|
if ymlFiles:
|
||||||
ymlPath = os.path.join(root, ymlFiles[0])
|
ymlPath = os.path.join(root, ymlFiles[0])
|
||||||
with open(ymlPath, 'r', encoding='utf-8') as f:
|
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
|
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"
|
"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 };
|
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
|
||||||
// Base visual diameter and hit area used for port and anchor handles.
|
// Base visual diameter and hit area used for port and anchor handles.
|
||||||
const PORT_NODE_SIZE = 30;
|
const PORT_NODE_SIZE = 30;
|
||||||
// Narrow anchor body width used in the canvas visual representation.
|
const PORT_LABEL_MIN_CHARS = 5;
|
||||||
const ANCHOR_NODE_WIDTH = 8;
|
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.
|
// Default spacing between repeated anchor or port pins.
|
||||||
const DEFAULT_ELEMENT_PITCH = 10;
|
const DEFAULT_ELEMENT_PITCH = 10;
|
||||||
// Defines built-in port and anchor element metadata before per-node expansion.
|
// Defines built-in port and anchor element metadata before per-node expansion.
|
||||||
@@ -477,7 +480,7 @@
|
|||||||
const nextY = x * sin + y * cos;
|
const nextY = x * sin + y * cos;
|
||||||
x = nextX;
|
x = nextX;
|
||||||
y = nextY;
|
y = nextY;
|
||||||
angle += rotation;
|
angle -= rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -489,16 +492,86 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create ordered React Flow handles for all ports on a single visual side.
|
// 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';
|
const vertical = side === 'left' || side === 'right';
|
||||||
|
|
||||||
return ports.map((port, index) => {
|
return ports.map((port, index) => {
|
||||||
const explicitPercent = Number(port.info && port.info.handlePercent);
|
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 percentValue = `${percent}%`;
|
||||||
const style = vertical
|
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 {
|
return {
|
||||||
name: port.name,
|
name: port.name,
|
||||||
@@ -511,13 +584,18 @@
|
|||||||
|
|
||||||
// Group transformed ports into canvas handles with side and position styling.
|
// Group transformed ports into canvas handles with side and position styling.
|
||||||
const buildPortHandles = (ports, transform) => {
|
const buildPortHandles = (ports, transform) => {
|
||||||
|
const options = transform || {};
|
||||||
const grouped = { left: [], right: [], top: [], bottom: [] };
|
const grouped = { left: [], right: [], top: [], bottom: [] };
|
||||||
|
const allPorts = [];
|
||||||
Object.entries(ports || {}).forEach(([name, info]) => {
|
Object.entries(ports || {}).forEach(([name, info]) => {
|
||||||
if (name === 'a0' || name === 'b0') return;
|
if (name === 'a0' || name === 'b0') return;
|
||||||
const transformedInfo = transformPortInfo(info, transform);
|
const transformedInfo = transformPortInfo(info, options);
|
||||||
const side = portSideFromAngle(transformedInfo.a);
|
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 => {
|
Object.values(grouped).forEach(sidePorts => {
|
||||||
sidePorts.sort((a, b) => {
|
sidePorts.sort((a, b) => {
|
||||||
@@ -529,10 +607,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...buildSideHandles(grouped.left, 'left'),
|
...buildSideHandles(grouped.left, 'left', metrics),
|
||||||
...buildSideHandles(grouped.right, 'right'),
|
...buildSideHandles(grouped.right, 'right', metrics),
|
||||||
...buildSideHandles(grouped.top, 'top'),
|
...buildSideHandles(grouped.top, 'top', metrics),
|
||||||
...buildSideHandles(grouped.bottom, 'bottom')
|
...buildSideHandles(grouped.bottom, 'bottom', metrics)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -615,6 +693,35 @@
|
|||||||
return name || (node && node.id) || 'port';
|
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.
|
// 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');
|
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.
|
// Detect built-in port or anchor nodes for element YAML export.
|
||||||
@@ -658,8 +765,13 @@
|
|||||||
const portNumber = normalizePortNumber(data && data.portNumber);
|
const portNumber = normalizePortNumber(data && data.portNumber);
|
||||||
const pitch = normalizePitch(data && data.pitch);
|
const pitch = normalizePitch(data && data.pitch);
|
||||||
const handleClearance = Math.max(pitch, 14);
|
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 {
|
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)
|
height: Math.max(PORT_NODE_SIZE, PORT_NODE_SIZE + Math.max(0, portNumber - 1) * handleClearance)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -695,7 +807,7 @@
|
|||||||
if (portNumber > 1) {
|
if (portNumber > 1) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
Array.from({ length: portNumber }, (_, index) => {
|
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([`a${index + 1}`, { x: 0, y, a: 180, width }]);
|
||||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, 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
|
// Export standalone Port pins as the outward-facing pin angle.
|
||||||
// angle used when this canvas is placed as a component elsewhere.
|
|
||||||
const externalPortAngle = (angle) => normalizeAngle(Number(angle ?? 0) + 180);
|
const externalPortAngle = (angle) => normalizeAngle(Number(angle ?? 0) + 180);
|
||||||
|
|
||||||
// Convert standalone port nodes into page-level layout ports.
|
// Convert standalone port nodes into page-level layout pins.
|
||||||
const buildPageComponentPorts = (port, nodes) => {
|
const buildPageComponentPins = (port, nodes) => {
|
||||||
const portNodes = (nodes || []).filter(isPortElementNode);
|
const portNodes = (nodes || []).filter(isPortElementNode);
|
||||||
if (portNodes.length > 0) {
|
if (portNodes.length > 0) {
|
||||||
return portNodes.reduce((ports, node) => {
|
return portNodes.reduce((pins, node) => {
|
||||||
const data = node.data || {};
|
const data = node.data || {};
|
||||||
const baseName = getNodePortName(node);
|
const baseName = getNodePortName(node);
|
||||||
const elementPorts = buildElementPorts('port', data);
|
const elementPorts = buildElementPorts('port', data);
|
||||||
const entries = Object.entries(elementPorts);
|
const entries = Object.entries(elementPorts);
|
||||||
entries.forEach(([portName, portInfo]) => {
|
entries.forEach(([portName, portInfo]) => {
|
||||||
const exportName = entries.length === 1
|
const exportName = getElementPinName(node, portName);
|
||||||
? baseName
|
|
||||||
: `${baseName}_${portName.replace(/^port_/, '')}`;
|
|
||||||
const point = getNodePortCanvasPoint(node, portName) || {
|
const point = getNodePortCanvasPoint(node, portName) || {
|
||||||
x: Number((node.position && node.position.x) || 0),
|
x: Number((node.position && node.position.x) || 0),
|
||||||
y: Number((node.position && node.position.y) || 0)
|
y: Number((node.position && node.position.y) || 0)
|
||||||
};
|
};
|
||||||
ports[exportName] = {
|
pins[exportName] = {
|
||||||
|
element: baseName,
|
||||||
|
pin: pinRoleFromElementPortName('port', portName),
|
||||||
x: Number(point.x || 0),
|
x: Number(point.x || 0),
|
||||||
y: Number(point.y || 0),
|
y: Number(point.y || 0),
|
||||||
a: externalPortAngle(portInfo.a ?? data.angle ?? data.a ?? 0),
|
a: externalPortAngle(portInfo.a ?? data.angle ?? data.a ?? 0),
|
||||||
width: Number(portInfo.width || data.width || 0.5)
|
width: Number(portInfo.width || data.width || 0.5)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return ports;
|
return pins;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
if (!port) return {};
|
if (!port) return {};
|
||||||
return {
|
return {
|
||||||
port: {
|
port_io1: {
|
||||||
|
element: 'port',
|
||||||
|
pin: 'io1',
|
||||||
x: Number(port.x || 0),
|
x: Number(port.x || 0),
|
||||||
y: Number(port.y || 0),
|
y: Number(port.y || 0),
|
||||||
a: externalPortAngle(port.a || 0),
|
a: externalPortAngle(port.a || 0),
|
||||||
@@ -813,27 +926,34 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize standalone canvas ports into a layout ports YAML section.
|
// Backward-compatible helper name for callers that still use the old JS API.
|
||||||
const buildCanvasPortsYaml = (nodes, fallbackPort) => {
|
const buildPageComponentPorts = buildPageComponentPins;
|
||||||
const ports = buildPageComponentPorts(fallbackPort, nodes);
|
|
||||||
const entries = Object.entries(ports);
|
// Serialize standalone canvas pins into a layout pins YAML section.
|
||||||
if (entries.length === 0) return 'ports: []';
|
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 sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node]));
|
||||||
const lines = entries.map(([name, info]) => {
|
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)}` : '';
|
const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : '';
|
||||||
return `- name: ${name}
|
return `- name: ${name}
|
||||||
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
|
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
|
||||||
|
element: ${info.element}
|
||||||
|
pin: ${info.pin}
|
||||||
x: ${Number(info.x || 0).toFixed(1)}
|
x: ${Number(info.x || 0).toFixed(1)}
|
||||||
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
||||||
angle: ${Number(info.a || 0).toFixed(1)}
|
angle: ${Number(info.a || 0).toFixed(1)}
|
||||||
width: ${Number(info.width || 0.5)}${description}`;
|
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.
|
// 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.
|
// Serialize built-in port and anchor nodes into layout element metadata.
|
||||||
const buildElementsYaml = (nodes) => {
|
const buildElementsYaml = (nodes) => {
|
||||||
@@ -845,16 +965,21 @@
|
|||||||
const angle = data.elementType === 'port' ? data.angle : data.rotation;
|
const angle = data.elementType === 'port' ? data.angle : data.rotation;
|
||||||
const portNumber = normalizePortNumber(data.portNumber);
|
const portNumber = normalizePortNumber(data.portNumber);
|
||||||
const pitch = normalizePitch(data.pitch);
|
const pitch = normalizePitch(data.pitch);
|
||||||
|
const pinLines = buildElementPinEntries(node)
|
||||||
|
.map(pin => ` - name: ${pin.name}\n role: ${pin.role}`)
|
||||||
|
.join('\n');
|
||||||
return ` ${name}:
|
return ` ${name}:
|
||||||
type: ${data.elementType}
|
type: ${data.elementType}
|
||||||
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
|
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
|
||||||
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
|
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
|
||||||
angle: ${Number(angle || 0).toFixed(1)}
|
angle: ${Number(angle || 0).toFixed(1)}
|
||||||
port_number: ${portNumber}
|
pin_number: ${portNumber}
|
||||||
pitch: ${Number(pitch)}
|
pitch: ${Number(pitch)}
|
||||||
layer: ${data.layer || 'WG_CORE'}
|
layer: ${data.layer || 'WG_CORE'}
|
||||||
width: ${Number(data.width || 0.5)}
|
width: ${Number(data.width || 0.5)}
|
||||||
description: ${toYamlScalar(data.description || '')}`;
|
description: ${toYamlScalar(data.description || '')}
|
||||||
|
pins:
|
||||||
|
${pinLines}`;
|
||||||
});
|
});
|
||||||
return `elements:\n${lines.join('\n')}`;
|
return `elements:\n${lines.join('\n')}`;
|
||||||
};
|
};
|
||||||
@@ -872,8 +997,12 @@
|
|||||||
const targetNode = nodeMap[edge.target];
|
const targetNode = nodeMap[edge.target];
|
||||||
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
||||||
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
||||||
const fromPort = edge.sourceHandle || 'unknown';
|
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
|
||||||
const toPort = edge.targetHandle || 'unknown';
|
? 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 route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
||||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
||||||
@@ -926,7 +1055,12 @@ ${linksYaml}`;
|
|||||||
const ports = buildElementPorts('port', node.data);
|
const ports = buildElementPorts('port', node.data);
|
||||||
const portInfo = ports && portName ? ports[portName] : ports.port;
|
const portInfo = ports && portName ? ports[portName] : ports.port;
|
||||||
if (!portInfo) return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
|
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 {
|
return {
|
||||||
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
|
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
|
||||||
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
|
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
|
||||||
@@ -1127,12 +1261,16 @@ ${linksYaml}`;
|
|||||||
getNodePortCanvasPoint,
|
getNodePortCanvasPoint,
|
||||||
buildPortHandles,
|
buildPortHandles,
|
||||||
buildElementPorts,
|
buildElementPorts,
|
||||||
|
buildElementPinEntries,
|
||||||
|
getElementPinName,
|
||||||
buildElementBoxSize,
|
buildElementBoxSize,
|
||||||
buildBasicComponentPorts,
|
buildBasicComponentPorts,
|
||||||
getBasicComponentMetadata,
|
getBasicComponentMetadata,
|
||||||
buildInstanceYaml,
|
buildInstanceYaml,
|
||||||
buildInstancesYaml,
|
buildInstancesYaml,
|
||||||
buildPageComponentPorts,
|
buildPageComponentPorts,
|
||||||
|
buildPageComponentPins,
|
||||||
|
buildCanvasPinsYaml,
|
||||||
buildCanvasPortsYaml,
|
buildCanvasPortsYaml,
|
||||||
buildBundlesYaml,
|
buildBundlesYaml,
|
||||||
buildPortsYaml,
|
buildPortsYaml,
|
||||||
|
|||||||
+398
-86
@@ -662,6 +662,18 @@ Organization : OptiHK Limited
|
|||||||
transform: translateY(-1px);
|
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 {
|
body.light-mode .mini-btn {
|
||||||
background: var(--mini-button-bg);
|
background: var(--mini-button-bg);
|
||||||
border-color: rgba(30, 48, 69, 0.18);
|
border-color: rgba(30, 48, 69, 0.18);
|
||||||
@@ -697,7 +709,10 @@ Organization : OptiHK Limited
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
max-width: calc(100% - 30px);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -723,6 +738,71 @@ Organization : OptiHK Limited
|
|||||||
color: #102033;
|
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 {
|
.build-layout-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
@@ -1195,6 +1275,34 @@ Organization : OptiHK Limited
|
|||||||
pointer-events: none;
|
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 {
|
.build-progress {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -1459,11 +1567,12 @@ Organization : OptiHK Limited
|
|||||||
calculateCompositeBoxSize,
|
calculateCompositeBoxSize,
|
||||||
buildPortHandles,
|
buildPortHandles,
|
||||||
buildElementPorts,
|
buildElementPorts,
|
||||||
|
getElementPinName,
|
||||||
buildElementBoxSize,
|
buildElementBoxSize,
|
||||||
getBasicComponentMetadata,
|
getBasicComponentMetadata,
|
||||||
buildInstancesYaml,
|
buildInstancesYaml,
|
||||||
buildPageComponentPorts,
|
buildPageComponentPorts,
|
||||||
buildCanvasPortsYaml,
|
buildCanvasPinsYaml,
|
||||||
buildElementsYaml,
|
buildElementsYaml,
|
||||||
buildBundlesYaml: buildRouteBundlesYaml,
|
buildBundlesYaml: buildRouteBundlesYaml,
|
||||||
normalizeAngle,
|
normalizeAngle,
|
||||||
@@ -1599,9 +1708,9 @@ Organization : OptiHK Limited
|
|||||||
}, [id, data.ports, data.componentName, data.boxSize]);
|
}, [id, data.ports, data.componentName, data.boxSize]);
|
||||||
|
|
||||||
const baseHandleStyle = {
|
const baseHandleStyle = {
|
||||||
width: 8, height: 8,
|
width: 6, height: 6,
|
||||||
background: 'var(--bg-main)',
|
background: 'var(--bg-main)',
|
||||||
border: '2px solid var(--accent)',
|
border: '1px solid var(--accent)',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
};
|
};
|
||||||
const handlePositionMap = {
|
const handlePositionMap = {
|
||||||
@@ -1610,27 +1719,32 @@ Organization : OptiHK Limited
|
|||||||
top: Position.Top,
|
top: Position.Top,
|
||||||
bottom: Position.Bottom
|
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 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 isAnchorElement = data.elementType === 'anchor';
|
||||||
const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName);
|
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 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 iconSize = createComponentSymbolMetrics(componentSize);
|
||||||
const portLabelStyle = (portHandle) => {
|
const portLabelStyle = (portHandle) => {
|
||||||
const base = { ...portHandle.style };
|
const base = { ...portHandle.style };
|
||||||
if (portHandle.position === 'left') {
|
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') {
|
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') {
|
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 (
|
return (
|
||||||
@@ -1651,7 +1765,7 @@ Organization : OptiHK Limited
|
|||||||
height: visualSize.height,
|
height: visualSize.height,
|
||||||
minHeight: visualSize.height,
|
minHeight: visualSize.height,
|
||||||
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
|
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)',
|
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
|
||||||
...(isBasicCompactComponent ? {
|
...(isBasicCompactComponent ? {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -1684,22 +1798,34 @@ Organization : OptiHK Limited
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
width: componentSize.width,
|
||||||
|
height: visualSize.height,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}>
|
||||||
{portHandles.map((portHandle) => (
|
{portHandles.map((portHandle) => (
|
||||||
<React.Fragment key={portHandle.name}>
|
<React.Fragment key={portHandle.name}>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={handlePositionMap[portHandle.position]}
|
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||||
id={portHandle.name}
|
id={portHandle.name}
|
||||||
title={portHandle.name}
|
title={portHandle.name}
|
||||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10 }}
|
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={handlePositionMap[portHandle.position]}
|
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||||
id={portHandle.name}
|
id={portHandle.name}
|
||||||
title={portHandle.name}
|
title={portHandle.name}
|
||||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5 }}
|
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
|
||||||
/>
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{portHandles.map((portHandle) => (
|
||||||
|
<React.Fragment key={`label-${portHandle.name}`}>
|
||||||
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
||||||
{portHandle.name}
|
{portHandle.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -1727,16 +1853,34 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
|
|
||||||
// Renders standalone exported port elements with repeated port handles.
|
// 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 PortNode = ({ id, data, selected }) => {
|
||||||
const angle = data.angle ?? 0;
|
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 ports = buildElementPorts('port', data);
|
||||||
const elementSize = buildElementBoxSize(data);
|
const elementSize = buildElementBoxSize(data);
|
||||||
const localHandlePorts = Object.fromEntries(
|
const localHandlePorts = Object.fromEntries(
|
||||||
Object.entries(ports).map(([name, info]) => [name, { ...info, a: 0 }])
|
Object.entries(ports).map(([name, info]) => [name, { ...info, a: 0 }])
|
||||||
);
|
);
|
||||||
|
const localPortHandles = useMemo(
|
||||||
|
() => buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize }),
|
||||||
|
[localHandlePorts, elementSize]
|
||||||
|
);
|
||||||
const portHandles = useMemo(
|
const portHandles = useMemo(
|
||||||
() => buildPortHandles(localHandlePorts, { rotation: 0 }),
|
() => buildPortHandles(localHandlePorts, { rotation: canvasAngle }),
|
||||||
[localHandlePorts]
|
[localHandlePorts, canvasAngle]
|
||||||
|
);
|
||||||
|
const portDirectionMap = useMemo(
|
||||||
|
() => new Map(portHandles.map(handle => [handle.name, handle.position])),
|
||||||
|
[portHandles]
|
||||||
);
|
);
|
||||||
const handlePositionMap = {
|
const handlePositionMap = {
|
||||||
left: Position.Left,
|
left: Position.Left,
|
||||||
@@ -1746,28 +1890,62 @@ Organization : OptiHK Limited
|
|||||||
};
|
};
|
||||||
const baseHandleStyle = {
|
const baseHandleStyle = {
|
||||||
background: 'var(--accent)',
|
background: 'var(--accent)',
|
||||||
width: 6,
|
width: 5,
|
||||||
height: 6
|
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 (
|
return (
|
||||||
|
<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={{
|
<div style={{
|
||||||
width: elementSize.width, height: elementSize.height, borderRadius: 999,
|
width: elementSize.width, height: elementSize.height, borderRadius: 7,
|
||||||
|
position: 'relative',
|
||||||
|
boxSizing: 'border-box',
|
||||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
|
||||||
color: selected ? 'white' : 'var(--accent)',
|
color: selected ? 'white' : 'var(--accent)',
|
||||||
fontSize: 8, fontWeight: 'bold',
|
fontSize: 8, fontWeight: 'bold',
|
||||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||||
transform: `rotate(${angle}deg)`,
|
transform: `rotate(${canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`,
|
||||||
}}>
|
}}>
|
||||||
<span>P</span>
|
{localPortHandles.map((portHandle) => (
|
||||||
{portHandles.map((portHandle) => (
|
|
||||||
<React.Fragment key={portHandle.name}>
|
<React.Fragment key={portHandle.name}>
|
||||||
<Handle type="source" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
|
<Handle
|
||||||
<Handle type="target" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
|
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>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1776,17 +1954,18 @@ Organization : OptiHK Limited
|
|||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const anchorRotation = data.rotation || 0;
|
const anchorRotation = data.rotation || 0;
|
||||||
const anchorVisualRotation = -Number(anchorRotation || 0);
|
const anchorVisualRotation = -Number(anchorRotation || 0);
|
||||||
|
const anchorDisplayName = data.componentDisplayName || data.label || 'anchor';
|
||||||
const ports = buildElementPorts('anchor', data);
|
const ports = buildElementPorts('anchor', data);
|
||||||
const elementSize = buildElementBoxSize(data);
|
const elementSize = buildElementBoxSize(data);
|
||||||
const localAnchorHandlePorts = Object.fromEntries(
|
const localAnchorHandlePorts = Object.fromEntries(
|
||||||
Object.entries(ports).map(([name, info]) => [name, { ...info, a: name.startsWith('a') || name.startsWith('left') ? 180 : 0 }])
|
Object.entries(ports).map(([name, info]) => [name, { ...info, a: name.startsWith('a') || name.startsWith('left') ? 180 : 0 }])
|
||||||
);
|
);
|
||||||
const portHandles = useMemo(
|
const portHandles = useMemo(
|
||||||
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||||
[localAnchorHandlePorts, data.flip, data.flop]
|
[localAnchorHandlePorts, elementSize, data.flip, data.flop]
|
||||||
);
|
);
|
||||||
const anchorDirectionHandles = useMemo(
|
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]
|
[localAnchorHandlePorts, anchorRotation, data.flip, data.flop]
|
||||||
);
|
);
|
||||||
const handlePositionMap = {
|
const handlePositionMap = {
|
||||||
@@ -1796,8 +1975,8 @@ Organization : OptiHK Limited
|
|||||||
bottom: Position.Bottom
|
bottom: Position.Bottom
|
||||||
};
|
};
|
||||||
const baseHandleStyle = {
|
const baseHandleStyle = {
|
||||||
width: 6,
|
width: 5,
|
||||||
height: 6,
|
height: 5,
|
||||||
background: 'var(--accent)',
|
background: 'var(--accent)',
|
||||||
border: '1px solid var(--bg-main)',
|
border: '1px solid var(--bg-main)',
|
||||||
borderRadius: '50%'
|
borderRadius: '50%'
|
||||||
@@ -1806,18 +1985,10 @@ Organization : OptiHK Limited
|
|||||||
const name = String(portName || '');
|
const name = String(portName || '');
|
||||||
return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right';
|
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 anchorHandleVisualStyle = (portHandle, zIndex) => {
|
||||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||||
const localTop = anchorPortVisualTop(portHandle.name);
|
const localTop = portHandle.style?.top || '50%';
|
||||||
return {
|
return {
|
||||||
...baseHandleStyle,
|
...baseHandleStyle,
|
||||||
zIndex,
|
zIndex,
|
||||||
@@ -1828,29 +1999,44 @@ Organization : OptiHK Limited
|
|||||||
transform: 'translate(-50%, -50%)'
|
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(() => {
|
useEffect(() => {
|
||||||
updateNodeInternals(id);
|
updateNodeInternals(id);
|
||||||
}, [id, data.ports, data.rotation, data.flip, data.flop, updateNodeInternals]);
|
}, [id, data.ports, data.rotation, data.flip, data.flop, updateNodeInternals]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="anchor-node-shell" style={{ width: elementSize.width, height: elementSize.height }}>
|
||||||
position: 'relative',
|
<div className="component-floating-label" title={anchorDisplayName}>
|
||||||
|
<strong>{anchorDisplayName}</strong>
|
||||||
|
<span>Anchor</span>
|
||||||
|
</div>
|
||||||
|
<div className="anchor-visual-body" style={{
|
||||||
width: elementSize.width,
|
width: elementSize.width,
|
||||||
height: elementSize.height,
|
height: elementSize.height,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: selected ? 'white' : 'var(--accent)',
|
color: selected ? 'white' : 'var(--accent)',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||||
transform: `rotate(${anchorVisualRotation}deg)`,
|
transform: `rotate(${anchorVisualRotation}deg)`,
|
||||||
}}>
|
}}>
|
||||||
<span>A</span>
|
|
||||||
{portHandles.map((portHandle) => (
|
{portHandles.map((portHandle) => (
|
||||||
<React.Fragment key={portHandle.name}>
|
<React.Fragment key={portHandle.name}>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -1867,9 +2053,13 @@ Organization : OptiHK Limited
|
|||||||
title={portHandle.name}
|
title={portHandle.name}
|
||||||
style={anchorHandleVisualStyle(portHandle, 5)}
|
style={anchorHandleVisualStyle(portHandle, 5)}
|
||||||
/>
|
/>
|
||||||
|
<div className="port-pin-label" style={pinLabelStyle(portHandle)}>
|
||||||
|
<span style={pinLabelTextStyle}>{pinLabelFromPortName(portHandle.name)}</span>
|
||||||
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2919,7 +3109,7 @@ Organization : OptiHK Limited
|
|||||||
onUpdateNode(selectedNode.id, {
|
onUpdateNode(selectedNode.id, {
|
||||||
data: {
|
data: {
|
||||||
basicArguments: nextArguments,
|
basicArguments: nextArguments,
|
||||||
ports: metadata?.ports || {},
|
ports: metadata?.pins || metadata?.ports || {},
|
||||||
boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize
|
boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3558,6 +3748,10 @@ Organization : OptiHK Limited
|
|||||||
const [rulerStartPoint, setRulerStartPoint] = useState(null);
|
const [rulerStartPoint, setRulerStartPoint] = useState(null);
|
||||||
const [rulerEndPoint, setRulerEndPoint] = useState(null);
|
const [rulerEndPoint, setRulerEndPoint] = useState(null);
|
||||||
const [rulerPreviewPoint, setRulerPreviewPoint] = 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 [projectTechnology, setProjectTechnology] = useState('');
|
||||||
const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST);
|
const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||||
const [currentLinkXsection, setCurrentLinkXsection] = useState('strip');
|
const [currentLinkXsection, setCurrentLinkXsection] = useState('strip');
|
||||||
@@ -3565,6 +3759,7 @@ Organization : OptiHK Limited
|
|||||||
const [clipboard, setClipboard] = useState({ nodes: [] });
|
const [clipboard, setClipboard] = useState({ nodes: [] });
|
||||||
|
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
|
const canvasViewportRef = useRef(null);
|
||||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||||
|
|
||||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||||
@@ -3602,6 +3797,14 @@ Organization : OptiHK Limited
|
|||||||
[rulerStartPoint, rulerActiveEndPoint]
|
[rulerStartPoint, rulerActiveEndPoint]
|
||||||
);
|
);
|
||||||
const rulerPreviewMeasurement = !rulerEndPoint && rulerPreviewPoint ? rulerMeasurement : null;
|
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.
|
// Normalizes free-route control points and removes adjacent duplicates before storage.
|
||||||
const compactRoutePoints = useCallback((points) => {
|
const compactRoutePoints = useCallback((points) => {
|
||||||
return (points || [])
|
return (points || [])
|
||||||
@@ -3743,7 +3946,7 @@ Organization : OptiHK Limited
|
|||||||
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
|
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
|
||||||
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
|
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
|
||||||
const handles = buildPortHandles(buildElementPorts('anchor', node.data), {
|
const handles = buildPortHandles(buildElementPorts('anchor', node.data), {
|
||||||
rotation: Number(node.data?.rotation || 0),
|
rotation: -Number(node.data?.rotation || 0),
|
||||||
flip: Boolean(node.data?.flip),
|
flip: Boolean(node.data?.flip),
|
||||||
flop: Boolean(node.data?.flop)
|
flop: Boolean(node.data?.flop)
|
||||||
});
|
});
|
||||||
@@ -4414,10 +4617,7 @@ Organization : OptiHK Limited
|
|||||||
if (!element || typeof element !== 'object') return;
|
if (!element || typeof element !== 'object') return;
|
||||||
const elementType = element.type === 'anchor' ? 'anchor' : (element.type === 'port' ? 'port' : '');
|
const elementType = element.type === 'anchor' ? 'anchor' : (element.type === 'port' ? 'port' : '');
|
||||||
if (!elementType) return;
|
if (!elementType) return;
|
||||||
if (elementType === 'port' && elementName === 'port' && Array.isArray(doc.ports) && doc.ports.length > 0) {
|
const portNumberValue = Math.floor(Number(element.pin_number ?? element.pinNumber ?? element.port_number ?? element.portNumber ?? 1));
|
||||||
return;
|
|
||||||
}
|
|
||||||
const portNumberValue = Math.floor(Number(element.port_number ?? element.portNumber ?? 1));
|
|
||||||
const portNumber = Number.isFinite(portNumberValue) ? Math.max(1, portNumberValue) : 1;
|
const portNumber = Number.isFinite(portNumberValue) ? Math.max(1, portNumberValue) : 1;
|
||||||
const pitchValue = Number(element.pitch ?? DEFAULT_ELEMENT_PITCH);
|
const pitchValue = Number(element.pitch ?? DEFAULT_ELEMENT_PITCH);
|
||||||
const pitch = Number.isFinite(pitchValue) ? Math.max(0, pitchValue) : DEFAULT_ELEMENT_PITCH;
|
const pitch = Number.isFinite(pitchValue) ? Math.max(0, pitchValue) : DEFAULT_ELEMENT_PITCH;
|
||||||
@@ -4436,6 +4636,7 @@ Organization : OptiHK Limited
|
|||||||
pitch,
|
pitch,
|
||||||
layer: element.layer || 'WG_CORE',
|
layer: element.layer || 'WG_CORE',
|
||||||
description: element.description || '',
|
description: element.description || '',
|
||||||
|
pinNames: Object.fromEntries((element.pins || []).map(pin => [pin.role, pin.name]).filter(([role, name]) => role && name)),
|
||||||
boxSize: buildElementBoxSize({ elementType, portNumber, pitch })
|
boxSize: buildElementBoxSize({ elementType, portNumber, pitch })
|
||||||
};
|
};
|
||||||
const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||||
@@ -4479,6 +4680,14 @@ Organization : OptiHK Limited
|
|||||||
return nodes;
|
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(() => {
|
useEffect(() => {
|
||||||
const input = document.getElementById('open-yaml-input');
|
const input = document.getElementById('open-yaml-input');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
@@ -4547,7 +4756,7 @@ Organization : OptiHK Limited
|
|||||||
componentDisplayName: instName,
|
componentDisplayName: instName,
|
||||||
type: isProject ? 'composite' : undefined,
|
type: isProject ? 'composite' : undefined,
|
||||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
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,
|
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||||
@@ -4569,13 +4778,17 @@ Organization : OptiHK Limited
|
|||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetId = nodeNameMap[toInst];
|
||||||
if (sourceId && targetId) {
|
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);
|
const view = routeStyleForSettings(route, false);
|
||||||
newEdges.push({
|
newEdges.push({
|
||||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
target: targetId,
|
target: targetId,
|
||||||
sourceHandle: fromPort,
|
sourceHandle,
|
||||||
targetHandle: toPort,
|
targetHandle,
|
||||||
type: view.type,
|
type: view.type,
|
||||||
style: view.style,
|
style: view.style,
|
||||||
data: { route, points: routePoints },
|
data: { route, points: routePoints },
|
||||||
@@ -4591,8 +4804,8 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
||||||
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
||||||
const importedPort = Array.isArray(doc.ports) && doc.ports[0]
|
const importedPin = Array.isArray(doc.pins) && doc.pins[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) }
|
? { 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 };
|
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||||
const newPage = {
|
const newPage = {
|
||||||
id: newPageId,
|
id: newPageId,
|
||||||
@@ -4603,8 +4816,8 @@ Organization : OptiHK Limited
|
|||||||
{
|
{
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
type: 'portNode',
|
type: 'portNode',
|
||||||
position: { x: importedPort.x, y: importedPort.y },
|
position: { x: importedPin.x, y: importedPin.y },
|
||||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPort.a, width: importedPort.width || 0.5, layer: 'WG_CORE', description: '' },
|
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPin.a, width: importedPin.width || 0.5, layer: 'WG_CORE', description: '' },
|
||||||
draggable: true,
|
draggable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
@@ -4612,7 +4825,7 @@ Organization : OptiHK Limited
|
|||||||
...newNodes,
|
...newNodes,
|
||||||
],
|
],
|
||||||
edges: newEdges,
|
edges: newEdges,
|
||||||
port: importedPort,
|
port: importedPin,
|
||||||
};
|
};
|
||||||
|
|
||||||
setPages(prev => [...prev, newPage]);
|
setPages(prev => [...prev, newPage]);
|
||||||
@@ -4663,7 +4876,7 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
input.addEventListener('change', handleFile);
|
input.addEventListener('change', handleFile);
|
||||||
return () => input.removeEventListener('change', handleFile);
|
return () => input.removeEventListener('change', handleFile);
|
||||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectCompositeMap(prev => {
|
setProjectCompositeMap(prev => {
|
||||||
@@ -4713,9 +4926,9 @@ Organization : OptiHK Limited
|
|||||||
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
||||||
const doc = jsyaml.load(content) || {};
|
const doc = jsyaml.load(content) || {};
|
||||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||||
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
|
||||||
const pagePort = firstPort
|
const pagePort = firstPin
|
||||||
? { 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) }
|
? { 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 };
|
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||||
const nodeNameMap = {};
|
const nodeNameMap = {};
|
||||||
const nodes = [
|
const nodes = [
|
||||||
@@ -4760,7 +4973,7 @@ Organization : OptiHK Limited
|
|||||||
componentDisplayName: instName,
|
componentDisplayName: instName,
|
||||||
type: instIsComposite ? 'composite' : undefined,
|
type: instIsComposite ? 'composite' : undefined,
|
||||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
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,
|
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||||
@@ -4781,13 +4994,17 @@ Organization : OptiHK Limited
|
|||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetId = nodeNameMap[toInst];
|
||||||
if (!sourceId || !targetId) return;
|
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);
|
const view = routeStyleForSettings(route, false);
|
||||||
edges.push({
|
edges.push({
|
||||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
target: targetId,
|
target: targetId,
|
||||||
sourceHandle: fromPort,
|
sourceHandle,
|
||||||
targetHandle: toPort,
|
targetHandle,
|
||||||
type: view.type,
|
type: view.type,
|
||||||
style: view.style,
|
style: view.style,
|
||||||
data: { route, points: routePoints },
|
data: { route, points: routePoints },
|
||||||
@@ -4861,7 +5078,7 @@ Organization : OptiHK Limited
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadProject();
|
loadProject();
|
||||||
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
||||||
@@ -4949,7 +5166,7 @@ Organization : OptiHK Limited
|
|||||||
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, boxSize),
|
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, boxSize),
|
||||||
data: {
|
data: {
|
||||||
...node.data,
|
...node.data,
|
||||||
ports: metadata.ports || {},
|
ports: metadata.pins || metadata.ports || {},
|
||||||
boxSize,
|
boxSize,
|
||||||
foundry: metadata.foundry || '',
|
foundry: metadata.foundry || '',
|
||||||
process: metadata.process || ''
|
process: metadata.process || ''
|
||||||
@@ -5273,7 +5490,7 @@ Organization : OptiHK Limited
|
|||||||
type: 'composite',
|
type: 'composite',
|
||||||
category: null,
|
category: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
ports: parsedData.ports || {},
|
ports: parsedData.pins || parsedData.ports || {},
|
||||||
boxSize: compositeBoxSize
|
boxSize: compositeBoxSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5324,7 +5541,7 @@ Organization : OptiHK Limited
|
|||||||
type: 'composite',
|
type: 'composite',
|
||||||
category: null,
|
category: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
ports: parsedData.ports || {},
|
ports: parsedData.pins || parsedData.ports || {},
|
||||||
boxSize: compositeBoxSize
|
boxSize: compositeBoxSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5377,7 +5594,7 @@ Organization : OptiHK Limited
|
|||||||
libraryCategory: 'basic',
|
libraryCategory: 'basic',
|
||||||
category: 'basic',
|
category: 'basic',
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
ports: metadata?.ports || {},
|
ports: metadata?.pins || metadata?.ports || {},
|
||||||
boxSize: metadata ? normalizeBoxSize(metadata) : DEFAULT_COMPONENT_BOX_SIZE,
|
boxSize: metadata ? normalizeBoxSize(metadata) : DEFAULT_COMPONENT_BOX_SIZE,
|
||||||
basicArguments
|
basicArguments
|
||||||
},
|
},
|
||||||
@@ -5585,13 +5802,16 @@ Organization : OptiHK Limited
|
|||||||
setRulerStartPoint(null);
|
setRulerStartPoint(null);
|
||||||
setRulerEndPoint(null);
|
setRulerEndPoint(null);
|
||||||
setRulerPreviewPoint(null);
|
setRulerPreviewPoint(null);
|
||||||
|
} else {
|
||||||
|
setOriginPickMode(false);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Convert a pane click or pointer event into canvas ruler coordinates.
|
// Convert a pane click or pointer event into canvas coordinates.
|
||||||
const eventToRulerPoint = useCallback((event) => {
|
const eventToCanvasPoint = useCallback((event) => {
|
||||||
|
if (!reactFlowInstance || !event) return null;
|
||||||
const rawPoint = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
const rawPoint = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
return {
|
return {
|
||||||
x: Number(Math.min(activeCanvasSize.width, Math.max(0, rawPoint.x)).toFixed(3)),
|
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]);
|
}, [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.
|
// Set ruler start/end points from canvas clicks.
|
||||||
const handleRulerPaneClick = useCallback((event) => {
|
const handleRulerPaneClick = useCallback((event) => {
|
||||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const nextPoint = eventToRulerPoint(event);
|
const nextPoint = eventToCanvasPoint(event);
|
||||||
|
if (!nextPoint) return;
|
||||||
if (!rulerStartPoint || rulerEndPoint) {
|
if (!rulerStartPoint || rulerEndPoint) {
|
||||||
setRulerStartPoint(nextPoint);
|
setRulerStartPoint(nextPoint);
|
||||||
setRulerEndPoint(null);
|
setRulerEndPoint(null);
|
||||||
@@ -5618,14 +5867,47 @@ Organization : OptiHK Limited
|
|||||||
if (measurement) {
|
if (measurement) {
|
||||||
addLog(`Ruler distance: ${measurement.label}`);
|
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.
|
// Update the live ruler preview point while measuring.
|
||||||
const handleRulerMouseMove = useCallback((event) => {
|
const handleRulerMouseMove = useCallback((event) => {
|
||||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||||
if (!rulerStartPoint || rulerEndPoint) return;
|
if (!rulerStartPoint || rulerEndPoint) return;
|
||||||
setRulerPreviewPoint(eventToRulerPoint(event));
|
const nextPoint = eventToCanvasPoint(event);
|
||||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint]);
|
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.
|
// Select a route edge by id with optional additive selection.
|
||||||
const selectEdgeById = useCallback((edgeId, additive = false) => {
|
const selectEdgeById = useCallback((edgeId, additive = false) => {
|
||||||
@@ -5873,7 +6155,7 @@ type: ${page.type === 'project' ? 'project' : 'composite'}
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
|
||||||
# 1. External Ports (How this cell connects to the outside world)
|
# 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)
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
instances:`;
|
instances:`;
|
||||||
@@ -6098,8 +6380,11 @@ ${bundlesBlock}`;
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref={canvasViewportRef}
|
||||||
style={{ flex: 1, position: 'relative' }}
|
style={{ flex: 1, position: 'relative' }}
|
||||||
onMouseDownCapture={handleCanvasMouseDown}
|
onMouseDownCapture={handleCanvasMouseDown}
|
||||||
|
onMouseMoveCapture={handleCanvasMouseMove}
|
||||||
|
onMouseLeave={handleCanvasMouseLeave}
|
||||||
>
|
>
|
||||||
<div className="canvas-toolbar">
|
<div className="canvas-toolbar">
|
||||||
<span className="grid-snap-label">Snap to Grid</span>
|
<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'}>
|
<button className="mini-btn" onClick={toggleRulerMode} aria-pressed={rulerMode ? 'true' : 'false'}>
|
||||||
{rulerMode ? 'Ruler On' : 'Ruler'}
|
{rulerMode ? 'Ruler On' : 'Ruler'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{buildProgress.active && (
|
{buildProgress.active && (
|
||||||
@@ -6176,6 +6469,25 @@ ${bundlesBlock}`;
|
|||||||
</div>
|
</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' && (
|
{activePage && activePage.type !== 'layoutPreview' && (
|
||||||
<button
|
<button
|
||||||
onClick={handleBuildLayout}
|
onClick={handleBuildLayout}
|
||||||
@@ -6196,12 +6508,12 @@ ${bundlesBlock}`;
|
|||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={handleBasicConnection}
|
onConnect={handleBasicConnection}
|
||||||
onPaneClick={handleRulerPaneClick}
|
onPaneClick={handleCanvasPaneClick}
|
||||||
onPaneMouseMove={handleRulerMouseMove}
|
onPaneMouseMove={handleCanvasMouseMove}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onNodeClick={handleRulerPaneClick}
|
onNodeClick={handleCanvasNodeClick}
|
||||||
onNodeMouseMove={handleRulerMouseMove}
|
onNodeMouseMove={handleCanvasMouseMove}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeMouseDown={onNodeMouseDown}
|
onNodeMouseDown={onNodeMouseDown}
|
||||||
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ assert(
|
|||||||
'canvas.html should use buildInstancesYaml for layout instance export'
|
'canvas.html should use buildInstancesYaml for layout instance export'
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('buildCanvasPortsYaml(page.nodes)'),
|
canvasHtml.includes('buildCanvasPinsYaml(page.nodes)'),
|
||||||
'canvas.html should export ports from active canvas port nodes'
|
'canvas.html should export pins from active canvas port nodes'
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('buildPageComponentPorts(page.port, page.nodes)'),
|
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 === 'ep2b').position, 'top');
|
||||||
assert.deepStrictEqual(handles.find(handle => handle.name === 'ep2a').position, 'bottom');
|
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.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 === '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.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({
|
const uniformLeftHandles = helpers.buildPortHandles({
|
||||||
p_top: { x: -10, y: 300, a: 180 },
|
p_top: { x: -10, y: 300, a: 180 },
|
||||||
@@ -37,6 +41,46 @@ assert.deepStrictEqual(
|
|||||||
['15%', '50%', '85%'],
|
['15%', '50%', '85%'],
|
||||||
'ports on the same side should be uniformly spaced after sorting'
|
'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(
|
assert.deepStrictEqual(
|
||||||
helpers.normalizeBoxSize({ box_size: [946, 75] }),
|
helpers.normalizeBoxSize({ box_size: [946, 75] }),
|
||||||
@@ -168,6 +212,34 @@ assert.strictEqual(
|
|||||||
'bottom',
|
'bottom',
|
||||||
'rotating a component should rotate vertical port handle sides'
|
'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();
|
const args = helpers.createForgeArguments();
|
||||||
assert(Object.keys(args).length >= 10);
|
assert(Object.keys(args).length >= 10);
|
||||||
@@ -230,8 +302,10 @@ assert.deepStrictEqual(
|
|||||||
const ninetyBendHandles = helpers.buildPortHandles(helpers.buildBasicComponentPorts('90 bend', { radius: 15 }));
|
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').position, 'left');
|
||||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').style.top, '50%');
|
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').position, 'top');
|
||||||
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.left, '50%');
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.left, '50%');
|
||||||
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.top, 0);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
helpers.getBasicComponentMetadata('180 bend', { radius: 15 }).box_size,
|
helpers.getBasicComponentMetadata('180 bend', { radius: 15 }).box_size,
|
||||||
[25, 50],
|
[25, 50],
|
||||||
@@ -318,14 +392,17 @@ assert(projectInstancesYaml.includes('cell_1:'));
|
|||||||
assert(projectInstancesYaml.includes('component: canvas_1'));
|
assert(projectInstancesYaml.includes('component: canvas_1'));
|
||||||
|
|
||||||
const pagePortsYaml = helpers.buildPortsYaml({ x: 50, y: 150, a: 90 });
|
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('x: 50.0'));
|
||||||
assert(pagePortsYaml.includes('y: -150.0'));
|
assert(pagePortsYaml.includes('y: -150.0'));
|
||||||
assert(pagePortsYaml.includes('angle: -90.0'));
|
assert(pagePortsYaml.includes('angle: -90.0'));
|
||||||
|
|
||||||
const componentPorts = helpers.buildPageComponentPorts({ x: 12, y: -6, a: 180 });
|
const componentPorts = helpers.buildPageComponentPorts({ x: 12, y: -6, a: 180 });
|
||||||
assert.deepStrictEqual(componentPorts, {
|
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 = [
|
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, 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(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(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 }).a1, { x: 0, y: 6, a: 180, 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 }).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(
|
assert.deepStrictEqual(
|
||||||
helpers.getNodePortCanvasPoint({
|
helpers.getNodePortCanvasPoint({
|
||||||
id: 'anchor-rotated',
|
id: 'anchor-rotated',
|
||||||
@@ -391,10 +469,31 @@ assert.deepStrictEqual(
|
|||||||
{ x: 115, y: 200 },
|
{ x: 115, y: 200 },
|
||||||
'Anchor port endpoint coordinates should rotate with the anchor body'
|
'Anchor port endpoint coordinates should rotate with the anchor body'
|
||||||
);
|
);
|
||||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 1 }), { width: 30, height: 30 });
|
assert.deepStrictEqual(
|
||||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 1 }), { width: 8, height: 30 });
|
helpers.getNodePortCanvasPoint({
|
||||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 4, pitch: 10 }), { width: 8, height: 72 });
|
id: 'port-rotated',
|
||||||
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 4, pitch: 10 }), { width: 30, height: 72 });
|
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(
|
assert.deepStrictEqual(
|
||||||
helpers.buildPageComponentPorts(null, [{
|
helpers.buildPageComponentPorts(null, [{
|
||||||
id: 'port-array',
|
id: 'port-array',
|
||||||
@@ -403,14 +502,44 @@ assert.deepStrictEqual(
|
|||||||
data: { componentDisplayName: 'array', elementType: 'port', portNumber: 3, pitch: 10, width: 0.6 }
|
data: { componentDisplayName: 'array', elementType: 'port', portNumber: 3, pitch: 10, width: 0.6 }
|
||||||
}]),
|
}]),
|
||||||
{
|
{
|
||||||
array_1: { x: 100, y: 190, a: 180, width: 0.6 },
|
array_io1: { element: 'array', pin: 'io1', x: 100, y: 190, a: 180, width: 0.6 },
|
||||||
array_2: { x: 100, y: 200, a: 180, width: 0.6 },
|
array_io2: { element: 'array', pin: 'io2', x: 100, y: 200, a: 180, width: 0.6 },
|
||||||
array_3: { x: 100, y: 210, 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);
|
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('description: "input port"'));
|
||||||
assert(canvasPortsYaml.includes('width: 0.7'));
|
assert(canvasPortsYaml.includes('width: 0.7'));
|
||||||
assert(canvasPortsYaml.includes('y: -20.0'));
|
assert(canvasPortsYaml.includes('y: -20.0'));
|
||||||
@@ -422,7 +551,11 @@ assert(elementsYaml.includes('type: port'));
|
|||||||
assert(elementsYaml.includes('anchor_1:'));
|
assert(elementsYaml.includes('anchor_1:'));
|
||||||
assert(elementsYaml.includes('type: anchor'));
|
assert(elementsYaml.includes('type: anchor'));
|
||||||
assert(elementsYaml.includes('y: -20.0'));
|
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'));
|
assert(elementsYaml.includes('pitch: 10'));
|
||||||
|
|
||||||
const instancesWithoutElements = helpers.buildInstancesYaml({
|
const instancesWithoutElements = helpers.buildInstancesYaml({
|
||||||
@@ -435,7 +568,7 @@ assert(instancesWithoutElements.includes('component_1:'));
|
|||||||
assert(instancesWithoutElements.includes('y: -60.0'));
|
assert(instancesWithoutElements.includes('y: -60.0'));
|
||||||
|
|
||||||
const multiPortComponentPorts = helpers.buildPageComponentPorts(null, elementNodes);
|
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 = {
|
const technologyManifest = {
|
||||||
defaults: { xsection: 'strip', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
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'),
|
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'
|
'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');
|
const routerDir = path.resolve(root, '..', 'mxpic_router', 'mxpic_router');
|
||||||
if (fs.existsSync(routerDir)) {
|
if (fs.existsSync(routerDir)) {
|
||||||
const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8');
|
const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8');
|
||||||
const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8');
|
const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8');
|
||||||
assert(
|
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('pitch: float = 10.0') &&
|
||||||
routerLoaderPy.includes('port_number=_int(element.get("port_number"'),
|
routerLoaderPy.includes('pins=_pins(element.get("pins"))'),
|
||||||
'mxpic_router loader should parse multi-port anchor metadata from exported elements'
|
'mxpic_router loader should parse pins-only layout metadata from exported elements'
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
routerBuilderPy.includes('for index in range(port_number):') &&
|
routerBuilderPy.includes('_port_element_pin_entries') &&
|
||||||
routerBuilderPy.includes('a{index + 1}') &&
|
routerBuilderPy.includes('_anchor_element_pin_entries') &&
|
||||||
routerBuilderPy.includes('b{index + 1}'),
|
routerBuilderPy.includes('_metadata_pins') &&
|
||||||
'mxpic_router builder should register aN/bN pins for multi-port anchors'
|
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 anchorRotation = data.rotation || 0') &&
|
||||||
canvasHtml.includes('const anchorVisualRotation = -Number(anchorRotation || 0)') &&
|
canvasHtml.includes('const anchorVisualRotation = -Number(anchorRotation || 0)') &&
|
||||||
canvasHtml.includes('transform: `rotate(${anchorVisualRotation}deg)`') &&
|
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('anchorDirectionHandles') &&
|
||||||
canvasHtml.includes('rotation: Number(anchorRotation || 0)') &&
|
canvasHtml.includes('rotation: -Number(anchorRotation || 0)') &&
|
||||||
canvasHtml.includes('anchorHandleVisualStyle(portHandle') &&
|
canvasHtml.includes('anchorHandleVisualStyle(portHandle') &&
|
||||||
canvasHtml.includes('anchorPortVisualSide') &&
|
canvasHtml.includes('anchorPortVisualSide') &&
|
||||||
canvasHtml.includes('portHandle.name') &&
|
canvasHtml.includes('portHandle.name') &&
|
||||||
canvasHtml.includes('visualSide === \'left\' ? 0 : elementSize.width') &&
|
canvasHtml.includes('visualSide === \'left\' ? 0 : elementSize.width') &&
|
||||||
canvasHtml.includes('anchorPortVisualTop') &&
|
canvasHtml.includes('transform: \'translate(-50%, -50%)\'') &&
|
||||||
canvasHtml.includes('(index - 1) / (portCount - 1)') &&
|
canvasHtml.includes("portHandle.style?.top || '50%'") &&
|
||||||
canvasHtml.includes('elementSize.height - baseHandleStyle.height') &&
|
|
||||||
canvasHtml.includes('localLeft') &&
|
canvasHtml.includes('localLeft') &&
|
||||||
canvasHtml.includes('localTop') &&
|
canvasHtml.includes('localTop') &&
|
||||||
canvasHtml.includes('handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]') &&
|
canvasHtml.includes('handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]') &&
|
||||||
canvasHtml.includes('getAnchorHandleRouteDirection') &&
|
canvasHtml.includes('getAnchorHandleRouteDirection') &&
|
||||||
canvasHtml.includes('rotation: Number(node.data?.rotation || 0)') &&
|
canvasHtml.includes('rotation: -Number(node.data?.rotation || 0)') &&
|
||||||
canvasHtml.includes('directionToReactFlowPosition') &&
|
canvasHtml.includes('directionToReactFlowPosition') &&
|
||||||
canvasHtml.includes('sourcePosition: directionToReactFlowPosition(sourceDirection)') &&
|
canvasHtml.includes('sourcePosition: directionToReactFlowPosition(sourceDirection)') &&
|
||||||
canvasHtml.includes('targetPosition: directionToReactFlowPosition(targetDirection)') &&
|
canvasHtml.includes('targetPosition: directionToReactFlowPosition(targetDirection)') &&
|
||||||
@@ -282,6 +281,16 @@ assert(
|
|||||||
canvasHtml.includes('component-floating-label') && canvasHtml.includes('component-visual-body'),
|
canvasHtml.includes('component-floating-label') && canvasHtml.includes('component-visual-body'),
|
||||||
'component labels should float outside the rotated 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(
|
assert(
|
||||||
canvasHtml.includes('canvasTextVisible') &&
|
canvasHtml.includes('canvasTextVisible') &&
|
||||||
canvasHtml.includes('toggleCanvasText') &&
|
canvasHtml.includes('toggleCanvasText') &&
|
||||||
@@ -353,11 +362,26 @@ assert(
|
|||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('Ruler') &&
|
canvasHtml.includes('Ruler') &&
|
||||||
canvasHtml.includes('rulerMode') &&
|
canvasHtml.includes('rulerMode') &&
|
||||||
canvasHtml.includes('onPaneClick={handleRulerPaneClick}') &&
|
canvasHtml.includes('onPaneClick={handleCanvasPaneClick}') &&
|
||||||
canvasHtml.includes('onNodeClick={handleRulerPaneClick}') &&
|
canvasHtml.includes('onNodeClick={handleCanvasNodeClick}') &&
|
||||||
canvasHtml.includes('onPaneMouseMove={handleRulerMouseMove}'),
|
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'
|
'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(
|
assert(
|
||||||
canvasHtml.includes('createRulerMeasurement') &&
|
canvasHtml.includes('createRulerMeasurement') &&
|
||||||
canvasHtml.includes('rulerPointNode') &&
|
canvasHtml.includes('rulerPointNode') &&
|
||||||
@@ -442,11 +466,42 @@ assert(
|
|||||||
canvasHtml.includes('font-size: 0.4rem;') &&
|
canvasHtml.includes('font-size: 0.4rem;') &&
|
||||||
canvasHtml.includes('font-size: 0.32rem;') &&
|
canvasHtml.includes('font-size: 0.32rem;') &&
|
||||||
canvasHtml.includes("font: 600 0.5rem/1.35") &&
|
canvasHtml.includes("font: 600 0.5rem/1.35") &&
|
||||||
canvasHtml.includes('width: 8, height: 8') &&
|
canvasHtml.includes('width: 6, height: 6') &&
|
||||||
canvasHtml.includes('width: 6,') &&
|
canvasHtml.includes("border: '1px solid var(--accent)'") &&
|
||||||
|
canvasHtml.includes('width: 5,') &&
|
||||||
canvasHtml.includes('fontSize: 8'),
|
canvasHtml.includes('fontSize: 8'),
|
||||||
'canvas labels and port circles should render smaller than the previous sizing'
|
'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(
|
assert(
|
||||||
canvasHtml.includes('ParallelRouteEdge') &&
|
canvasHtml.includes('ParallelRouteEdge') &&
|
||||||
canvasHtml.includes('parallelOffset') &&
|
canvasHtml.includes('parallelOffset') &&
|
||||||
|
|||||||
Reference in New Issue
Block a user