diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc index f338ff7..75a6379 100644 Binary files a/backend/__pycache__/database.cpython-39.pyc and b/backend/__pycache__/database.cpython-39.pyc differ diff --git a/backend/__pycache__/gds_builder.cpython-39.pyc b/backend/__pycache__/gds_builder.cpython-39.pyc index 4b794e9..38fd6bf 100644 Binary files a/backend/__pycache__/gds_builder.cpython-39.pyc and b/backend/__pycache__/gds_builder.cpython-39.pyc differ diff --git a/backend/__pycache__/layout_preview.cpython-39.pyc b/backend/__pycache__/layout_preview.cpython-39.pyc index a509278..36e98cb 100644 Binary files a/backend/__pycache__/layout_preview.cpython-39.pyc and b/backend/__pycache__/layout_preview.cpython-39.pyc differ diff --git a/backend/__pycache__/pdk_access.cpython-39.pyc b/backend/__pycache__/pdk_access.cpython-39.pyc index df11d38..be23ec1 100644 Binary files a/backend/__pycache__/pdk_access.cpython-39.pyc and b/backend/__pycache__/pdk_access.cpython-39.pyc differ diff --git a/backend/__pycache__/pdk_registry.cpython-39.pyc b/backend/__pycache__/pdk_registry.cpython-39.pyc index cab17b8..9a90baf 100644 Binary files a/backend/__pycache__/pdk_registry.cpython-39.pyc and b/backend/__pycache__/pdk_registry.cpython-39.pyc differ diff --git a/backend/__pycache__/routed_layout_preview.cpython-39.pyc b/backend/__pycache__/routed_layout_preview.cpython-39.pyc index e0a6574..832e6a2 100644 Binary files a/backend/__pycache__/routed_layout_preview.cpython-39.pyc and b/backend/__pycache__/routed_layout_preview.cpython-39.pyc differ diff --git a/backend/__pycache__/technology_manifest.cpython-39.pyc b/backend/__pycache__/technology_manifest.cpython-39.pyc index 94103d2..7f9f638 100644 Binary files a/backend/__pycache__/technology_manifest.cpython-39.pyc and b/backend/__pycache__/technology_manifest.cpython-39.pyc differ diff --git a/backend/gds_builder.py b/backend/gds_builder.py index 821510b..f5e0249 100644 --- a/backend/gds_builder.py +++ b/backend/gds_builder.py @@ -1,6 +1,6 @@ # ----------------------------------------------------------------------------- # Description: Backend integration wrapper for project GDS generation and build result handling. -# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _build_with_gdstk, _import_public_gds, _build_with_nazca, _safe_cell_name, _library_cell_by_name, _number +# Inside functions: build_project_gds, _build_with_mxpic_router, _load_project_cells, _ordered_cell_names, _cells_have_links, _cells_have_elements, _build_with_gdstk, _import_public_gds, _build_with_nazca, _build_nazca_element_cells, _build_nazca_element_cell, _element_port_offset, _safe_cell_name, _library_cell_by_name, _int, _number # Developer : Qin Yue @ 2026 # Organization : OptiHK Limited # ----------------------------------------------------------------------------- @@ -58,7 +58,19 @@ def build_project_gds( registry = PdkRegistry(pdk_public_root, prefer_full_gds=prefer_full_gds) os.makedirs(os.path.dirname(output_path), exist_ok=True) - # gdstk is the preferred fallback; Nazca remains a secondary fallback for + # Port/Anchor elements are logical pin-bearing devices. Use Nazca for these + # layouts so each element can be a placed cell with local nd.Pin metadata. + if _cells_have_elements(cells): + try: + return _build_with_nazca(cells, output_path, registry) + except ImportError as nazca_error: + raise RuntimeError( + "Build GDS with Port/Anchor elements requires Nazca so element cells can preserve nd.Pin metadata. " + f"Nazca import failed: {nazca_error}" + ) from nazca_error + + # gdstk is the preferred fallback for placement-only projects without + # Port/Anchor element pin metadata; Nazca remains a secondary fallback for # environments where gdstk is not installed. try: return _build_with_gdstk(cells, output_path, registry) @@ -132,6 +144,16 @@ def _cells_have_links(cells: Dict[str, dict]) -> bool: return False +def _cells_have_elements(cells: Dict[str, dict]) -> bool: + """Detect whether any saved cell contains Port/Anchor element objects.""" + for data in cells.values(): + elements = data.get("elements") or {} + for element in elements.values(): + if str((element or {}).get("type") or "").lower() in {"port", "anchor"}: + return True + return False + + def _build_with_gdstk(cells: Dict[str, dict], output_path: str, registry: PdkRegistry) -> BuildResult: """Assemble a project GDS with gdstk when Nazca or routed building is unavailable.""" import gdstk @@ -196,6 +218,7 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg # is exported as the top-level GDS. for cell_name in ordered_names: data = cells[cell_name] + element_cells = _build_nazca_element_cells(nd, cell_name, data.get("elements") or {}, built_cells) with nd.Cell(cell_name) as current_cell: for instance_name, instance in (data.get("instances") or {}).items(): component = str(instance.get("component") or "") @@ -211,6 +234,12 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg continue loaded = nd.load_gds(asset.gds_path) loaded.put(x, y, rotation) + for element_name, element_cell in element_cells.items(): + element = (data.get("elements") or {}).get(element_name) or {} + x = _number(element.get("x")) + y = _number(element.get("y")) + rotation = _number(element.get("angle")) + element_cell.put(x, y, rotation) built_cells[cell_name] = current_cell top_name = ordered_names[-1] @@ -218,6 +247,72 @@ def _build_with_nazca(cells: Dict[str, dict], output_path: str, registry: PdkReg return BuildResult(output_path=output_path, engine="nazca", cells_built=ordered_names, warnings=warnings) +def _build_nazca_element_cells(nd, parent_cell_name: str, elements: dict, existing_cells: dict) -> Dict[str, object]: + """Build reusable Nazca cells for built-in Port and Anchor element objects.""" + element_cells = {} + known_cells = dict(existing_cells or {}) + for element_name, element in (elements or {}).items(): + element_type = str((element or {}).get("type") or "").lower() + if element_type not in {"port", "anchor"}: + continue + raw_cell_name = f"{parent_cell_name}_{element_name}_{element_type}" + cell_name = _safe_cell_name(raw_cell_name, {**known_cells, **element_cells}) + element_cell = _build_nazca_element_cell(nd, cell_name, {**(element or {}), "name": str(element_name)}) + element_cells[str(element_name)] = element_cell + return element_cells + + +def _build_nazca_element_cell(nd, cell_name: str, element: dict): + """Create one local Port or Anchor cell whose pins are placed in local coordinates.""" + element = element or {} + element_type = str(element.get("type") or "").lower() + width = _number(element.get("width"), 0.5) + port_number = max(1, _int(element.get("pin_number", element.get("pinNumber", element.get("port_number", element.get("portNumber")))), 1)) + pitch = _number(element.get("pitch"), 10.0) + with nd.Cell(name=cell_name) as element_cell: + if element_type == "port": + for index, pin_name in enumerate(_element_pin_names(cell_name, element, port_number, "port")): + y = 0.0 if port_number == 1 else _element_port_offset(index, port_number, pitch) + nd.Pin(pin_name, width=width).put(0.0, y, 180.0) + return element_cell + + anchor_pin_names = _element_pin_names(cell_name, element, port_number, "anchor") + for index in range(port_number): + y = -15.0 + _element_port_offset(index, port_number, pitch) + nd.Pin(anchor_pin_names[index * 2], width=width).put(0.0, y, 180.0) + nd.Pin(anchor_pin_names[index * 2 + 1], width=width).put(0.0, y, 0.0) + return element_cell + + +def _element_port_offset(index: int, count: int, pitch: float) -> float: + """Return the local y offset for one repeated Port/Anchor pin.""" + return ((max(1, count) - 1) / 2 - index) * pitch + + +def _element_pin_names(cell_name: str, element: dict, count: int, element_type: str) -> List[str]: + """Return local element pin names, using YAML overrides when present.""" + raw_pins = [pin for pin in (element.get("pins") or []) if isinstance(pin, dict)] + default_base = _safe_pin_name(str(element.get("name") or cell_name).replace("_port", "").replace("_anchor", "")) + roles = [] + if element_type == "port": + roles = [f"io{index + 1}" for index in range(count)] + else: + for index in range(count): + roles.extend([f"a{index + 1}", f"b{index + 1}"]) + by_role = {str(pin.get("role") or ""): str(pin.get("name") or "") for pin in raw_pins} + by_index = [str(pin.get("name") or "") for pin in raw_pins] + names = [] + for index, role in enumerate(roles): + name = by_role.get(role) or (by_index[index] if index < len(by_index) else "") or f"{default_base}_{role}" + names.append(_safe_pin_name(name)) + return names + + +def _safe_pin_name(name: str) -> str: + """Generate a backend-safe pin name for local Nazca element pins.""" + return "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in str(name or "pin")) + + def _safe_cell_name(name: str, existing: dict) -> str: """Generate a backend-safe unique cell name for GDS/Nazca libraries.""" base = "".join(ch if ch.isalnum() or ch in "._$" else "_" for ch in str(name)) or "cell" @@ -238,6 +333,16 @@ def _library_cell_by_name(library, name: str): return None +def _int(value, default=0) -> int: + """Convert integer YAML values with a stable default.""" + try: + if value is None or value == "": + return default + return int(float(value)) + except (TypeError, ValueError): + return default + + def _number(value, default=0.0) -> float: """Convert numeric YAML values to floats with a stable default.""" try: diff --git a/backend/pdk_registry.py b/backend/pdk_registry.py index 085d6a8..ccd9386 100644 --- a/backend/pdk_registry.py +++ b/backend/pdk_registry.py @@ -107,7 +107,24 @@ class PdkRegistry: if not yaml_path: return None with open(yaml_path, "r", encoding="utf-8") as file: - return yaml.safe_load(file) or {} + data = yaml.safe_load(file) or {} + return self._normalize_pdk_pins(data) + + def _normalize_pdk_pins(self, data: dict) -> dict: + """Treat legacy PDK `ports` metadata as `pins` inside approved PDK roots.""" + if ( + self._allows_legacy_ports_as_pins() + and isinstance(data, dict) + and "pins" not in data + and isinstance(data.get("ports"), dict) + ): + data = dict(data) + data["pins"] = data["ports"] + return data + + def _allows_legacy_ports_as_pins(self) -> bool: + normalized = self.public_root.replace("\\", "/").lower() + return "/opt_pdk_public/" in f"{normalized}/" or "/opt_pdk_atlas/" in f"{normalized}/" def _inside_root(self, path: str) -> bool: """Check that a candidate asset path remains inside the permitted PDK root.""" diff --git a/backend/server.py b/backend/server.py index b5a24a3..8a0dcbd 100644 --- a/backend/server.py +++ b/backend/server.py @@ -360,7 +360,13 @@ def readCompYaml(compName, comps_root=None): if ymlFiles: ymlPath = os.path.join(root, ymlFiles[0]) with open(ymlPath, 'r', encoding='utf-8') as f: - return yaml.safe_load(f) + data = yaml.safe_load(f) or {} + normalized_root = os.path.abspath(search_root).replace("\\", "/").lower() + pdk_ports_allowed = "/opt_pdk_public/" in f"{normalized_root}/" or "/opt_pdk_atlas/" in f"{normalized_root}/" + if pdk_ports_allowed and isinstance(data, dict) and "pins" not in data and isinstance(data.get("ports"), dict): + data = dict(data) + data["pins"] = data["ports"] + return data return None diff --git a/database/_exports/009d30aca3e44bc6bdf43b0584e549fe/mxpic_project_1.gds b/database/_exports/009d30aca3e44bc6bdf43b0584e549fe/mxpic_project_1.gds new file mode 100644 index 0000000..bc8998c Binary files /dev/null and b/database/_exports/009d30aca3e44bc6bdf43b0584e549fe/mxpic_project_1.gds differ diff --git a/database/_exports/449a329fa1d14dea85e034c9de012e09/mxpic_project_1.gds b/database/_exports/449a329fa1d14dea85e034c9de012e09/mxpic_project_1.gds new file mode 100644 index 0000000..6af7506 Binary files /dev/null and b/database/_exports/449a329fa1d14dea85e034c9de012e09/mxpic_project_1.gds differ diff --git a/database/_exports/5ed74e6f5cb54b1eb7e1a3277bfb9834/mxpic_project_1.gds b/database/_exports/5ed74e6f5cb54b1eb7e1a3277bfb9834/mxpic_project_1.gds new file mode 100644 index 0000000..2ff2bf2 Binary files /dev/null and b/database/_exports/5ed74e6f5cb54b1eb7e1a3277bfb9834/mxpic_project_1.gds differ diff --git a/database/_exports/68783ecf8c704642ba4623366c49402f/mxpic_project_1.gds b/database/_exports/68783ecf8c704642ba4623366c49402f/mxpic_project_1.gds new file mode 100644 index 0000000..4de0fe6 Binary files /dev/null and b/database/_exports/68783ecf8c704642ba4623366c49402f/mxpic_project_1.gds differ diff --git a/database/_exports/6d77e66abe114ceabca364153d44ff89/mxpic_project_1.gds b/database/_exports/6d77e66abe114ceabca364153d44ff89/mxpic_project_1.gds new file mode 100644 index 0000000..b146e61 Binary files /dev/null and b/database/_exports/6d77e66abe114ceabca364153d44ff89/mxpic_project_1.gds differ diff --git a/database/_exports/6d9241698e134d6f8cd58bcc9a18ef63/mxpic_project_1.gds b/database/_exports/6d9241698e134d6f8cd58bcc9a18ef63/mxpic_project_1.gds new file mode 100644 index 0000000..dbc5d26 Binary files /dev/null and b/database/_exports/6d9241698e134d6f8cd58bcc9a18ef63/mxpic_project_1.gds differ diff --git a/database/_exports/752665558896485a9f1def48bd481041/mxpic_project_1.gds b/database/_exports/752665558896485a9f1def48bd481041/mxpic_project_1.gds new file mode 100644 index 0000000..91d7f32 Binary files /dev/null and b/database/_exports/752665558896485a9f1def48bd481041/mxpic_project_1.gds differ diff --git a/database/_exports/b73ed3f3e158431cb6f496c2d3a0d1af/mxpic_project_1.gds b/database/_exports/b73ed3f3e158431cb6f496c2d3a0d1af/mxpic_project_1.gds new file mode 100644 index 0000000..2927e40 Binary files /dev/null and b/database/_exports/b73ed3f3e158431cb6f496c2d3a0d1af/mxpic_project_1.gds differ diff --git a/database/_exports/c66d1207b54d467b902797c509bdad39/mxpic_project_1.gds b/database/_exports/c66d1207b54d467b902797c509bdad39/mxpic_project_1.gds new file mode 100644 index 0000000..4c8d337 Binary files /dev/null and b/database/_exports/c66d1207b54d467b902797c509bdad39/mxpic_project_1.gds differ diff --git a/database/_exports/e648ffaf59e343d49782770690607c60/mxpic_project_1.gds b/database/_exports/e648ffaf59e343d49782770690607c60/mxpic_project_1.gds new file mode 100644 index 0000000..b53f9a2 Binary files /dev/null and b/database/_exports/e648ffaf59e343d49782770690607c60/mxpic_project_1.gds differ diff --git a/database/admin/layout/mxpic_project_1/.project.json b/database/admin/layout/MZM_TX/.project.json similarity index 62% rename from database/admin/layout/mxpic_project_1/.project.json rename to database/admin/layout/MZM_TX/.project.json index 4f868b9..d0783bb 100644 --- a/database/admin/layout/mxpic_project_1/.project.json +++ b/database/admin/layout/MZM_TX/.project.json @@ -1,4 +1,4 @@ { - "name": "mxpic_project_1", + "name": "MZM_TX", "technology": "Silterra/EMO1_2ML_CU_Al_RDL" } \ No newline at end of file diff --git a/database/admin/layout/MZM_TX/MZM_TX.svg b/database/admin/layout/MZM_TX/MZM_TX.svg new file mode 100644 index 0000000..82ffe06 --- /dev/null +++ b/database/admin/layout/MZM_TX/MZM_TX.svg @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Parameters: +x = 0.0 +y = 4.475 +a = 180.0 +width = 0.7 +name = a1 +Parameters: +x = 0.0 +y = -4.475 +a = 180.0 +width = 0.7 +name = a2 +Parameters: +x = 1660.0 +y = 4.475 +a = 360.0 +width = 0.7 +name = b1 +Parameters: +x = 1660.0 +y = -4.475 +a = 360.0 +width = 0.7 +name = b2 +Parameters: +x = 1660.0 +y = 25.0 +a = 0.0 +width = 5 +name = pin_p +Parameters: +x = 1660.0 +y = 55.0 +a = 0.0 +width = 5 +name = pin_n +Parameters: +x = 1660.0 +y = -25.0 +a = 0.0 +width = 5 +name = ht_s +Parameters: +x = 1660.0 +y = -55.0 +a = 0.0 +width = 5 +name = ht_g + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database/admin/layout/MZM_TX/MZM_TX.yml b/database/admin/layout/MZM_TX/MZM_TX.yml new file mode 100644 index 0000000..6fdd59d --- /dev/null +++ b/database/admin/layout/MZM_TX/MZM_TX.yml @@ -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 \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/canvas_1.svg b/database/admin/layout/MZM_TX/Spliter_1x4.svg similarity index 62% rename from database/admin/layout/mxpic_project_1/canvas_1.svg rename to database/admin/layout/MZM_TX/Spliter_1x4.svg index 7638ddd..b93dbfa 100644 --- a/database/admin/layout/mxpic_project_1/canvas_1.svg +++ b/database/admin/layout/MZM_TX/Spliter_1x4.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database/admin/layout/MZM_TX/Spliter_1x4.yml b/database/admin/layout/MZM_TX/Spliter_1x4.yml new file mode 100644 index 0000000..13bddcb --- /dev/null +++ b/database/admin/layout/MZM_TX/Spliter_1x4.yml @@ -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 \ No newline at end of file diff --git a/database/admin/layout/MZM_TX/canvas_1.svg b/database/admin/layout/MZM_TX/canvas_1.svg new file mode 100644 index 0000000..28bba15 --- /dev/null +++ b/database/admin/layout/MZM_TX/canvas_1.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/canvas_1.yml b/database/admin/layout/mxpic_project_1/canvas_1.yml deleted file mode 100644 index 9510109..0000000 --- a/database/admin/layout/mxpic_project_1/canvas_1.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/mxpic_project_1.svg b/database/admin/layout/mxpic_project_1/mxpic_project_1.svg deleted file mode 100644 index 54e6946..0000000 --- a/database/admin/layout/mxpic_project_1/mxpic_project_1.svg +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/mxpic_project_1.yml b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml deleted file mode 100644 index e4fdb85..0000000 --- a/database/admin/layout/mxpic_project_1/mxpic_project_1.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/database/mxpic_data.db b/database/mxpic_data.db index 3005111..ee764bc 100644 Binary files a/database/mxpic_data.db and b/database/mxpic_data.db differ diff --git a/frontend/canvas-helpers.js b/frontend/canvas-helpers.js index 7dd018d..3b09dcb 100644 --- a/frontend/canvas-helpers.js +++ b/frontend/canvas-helpers.js @@ -22,8 +22,11 @@ const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 }; // Base visual diameter and hit area used for port and anchor handles. const PORT_NODE_SIZE = 30; - // Narrow anchor body width used in the canvas visual representation. - const ANCHOR_NODE_WIDTH = 8; + const PORT_LABEL_MIN_CHARS = 5; + const PORT_LABEL_CHAR_WIDTH = 7; + const PORT_LABEL_HORIZONTAL_PADDING = 12; + // Anchor body width used in the canvas visual representation. + const ANCHOR_NODE_WIDTH = 16; // Default spacing between repeated anchor or port pins. const DEFAULT_ELEMENT_PITCH = 10; // Defines built-in port and anchor element metadata before per-node expansion. @@ -477,7 +480,7 @@ const nextY = x * sin + y * cos; x = nextX; y = nextY; - angle += rotation; + angle -= rotation; } return { @@ -489,16 +492,86 @@ }; // Create ordered React Flow handles for all ports on a single visual side. - const buildSideHandles = (ports, side) => { + const clampPercent = (value) => roundPercent(Math.min(100, Math.max(0, value))); + + const createCoordinateMetrics = (ports, boxSize) => { + const width = positiveNumber(boxSize && boxSize.width); + const height = positiveNumber(boxSize && boxSize.height); + if (!width || !height) return null; + const values = { x: [], y: [] }; + ports.forEach(port => { + const info = port && port.info; + const x = Number(info && info.x); + const y = Number(info && info.y); + if (Number.isFinite(x)) values.x.push(x); + if (Number.isFinite(y)) values.y.push(y); + }); + return { width, height, values }; + }; + + const coordinateAxisMode = (axisValues, size) => { + if (!axisValues || axisValues.length === 0 || !size) return 'fallback'; + const min = Math.min(...axisValues); + const max = Math.max(...axisValues); + const epsilon = Math.max(0.001, size * 0.001); + if (Math.abs(max - min) <= epsilon) { + return Math.abs(max) <= epsilon ? 'centered' : 'fallback'; + } + if (min < -epsilon && max <= size / 2 + epsilon && min >= -size / 2 - epsilon) { + return 'centered'; + } + if (min >= -epsilon && max <= size + epsilon) { + return 'positive'; + } + return 'fallback'; + }; + + const coordinatePercent = (info, axis, metrics) => { + if (!metrics) return null; + const value = Number(info && info[axis]); + if (!Number.isFinite(value)) return null; + const size = axis === 'x' ? metrics.width : metrics.height; + const mode = coordinateAxisMode(metrics.values[axis], size); + if (mode === 'centered') { + return axis === 'x' + ? clampPercent(50 + (value / size) * 100) + : clampPercent(50 - (value / size) * 100); + } + if (mode === 'positive') { + return axis === 'x' + ? clampPercent((value / size) * 100) + : clampPercent(100 - (value / size) * 100); + } + return null; + }; + + const buildSideHandles = (ports, side, metrics) => { const vertical = side === 'left' || side === 'right'; return ports.map((port, index) => { const explicitPercent = Number(port.info && port.info.handlePercent); - const percent = Number.isFinite(explicitPercent) ? explicitPercent : fallbackPercent(index, ports.length); + const exactPercent = coordinatePercent(port.info, vertical ? 'y' : 'x', metrics); + const percent = Number.isFinite(explicitPercent) + ? explicitPercent + : exactPercent !== null + ? exactPercent + : fallbackPercent(index, ports.length); const percentValue = `${percent}%`; const style = vertical - ? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' } - : { left: percentValue, transform: side === 'top' ? 'translate(-50%, -50%)' : 'translate(-50%, 50%)' }; + ? { + left: side === 'left' ? 0 : '100%', + right: 'auto', + top: percentValue, + bottom: 'auto', + transform: 'translate(-50%, -50%)' + } + : { + left: percentValue, + right: 'auto', + top: side === 'top' ? 0 : '100%', + bottom: 'auto', + transform: 'translate(-50%, -50%)' + }; return { name: port.name, @@ -511,13 +584,18 @@ // Group transformed ports into canvas handles with side and position styling. const buildPortHandles = (ports, transform) => { + const options = transform || {}; const grouped = { left: [], right: [], top: [], bottom: [] }; + const allPorts = []; Object.entries(ports || {}).forEach(([name, info]) => { if (name === 'a0' || name === 'b0') return; - const transformedInfo = transformPortInfo(info, transform); + const transformedInfo = transformPortInfo(info, options); const side = portSideFromAngle(transformedInfo.a); - grouped[side].push({ name, info: transformedInfo }); + const port = { name, info: transformedInfo }; + grouped[side].push(port); + allPorts.push(port); }); + const metrics = createCoordinateMetrics(allPorts, options.boxSize); Object.values(grouped).forEach(sidePorts => { sidePorts.sort((a, b) => { @@ -529,10 +607,10 @@ }); return [ - ...buildSideHandles(grouped.left, 'left'), - ...buildSideHandles(grouped.right, 'right'), - ...buildSideHandles(grouped.top, 'top'), - ...buildSideHandles(grouped.bottom, 'bottom') + ...buildSideHandles(grouped.left, 'left', metrics), + ...buildSideHandles(grouped.right, 'right', metrics), + ...buildSideHandles(grouped.top, 'top', metrics), + ...buildSideHandles(grouped.bottom, 'bottom', metrics) ]; }; @@ -615,6 +693,35 @@ return name || (node && node.id) || 'port'; }; + const pinRoleFromElementPortName = (elementType, portName) => { + const name = String(portName || ''); + if (elementType === 'anchor') { + const anchorMatch = name.match(/^([ab])(\d+)$/); + return anchorMatch ? `${anchorMatch[1]}${anchorMatch[2]}` : name; + } + const portMatch = name.match(/^port_(\d+)$/); + return portMatch ? `io${portMatch[1]}` : 'io1'; + }; + + const defaultElementPinName = (elementName, role) => `${elementName}_${role}`; + + const getElementPinName = (node, portName) => { + const data = (node && node.data) || {}; + const elementType = data.elementType === 'anchor' ? 'anchor' : 'port'; + const elementName = getNodePortName(node); + const role = pinRoleFromElementPortName(elementType, portName); + return (data.pinNames && data.pinNames[role]) || defaultElementPinName(elementName, role); + }; + + const buildElementPinEntries = (node) => { + const data = (node && node.data) || {}; + const elementType = data.elementType === 'anchor' ? 'anchor' : 'port'; + return Object.keys(buildElementPorts(elementType, data)).map(portName => { + const role = pinRoleFromElementPortName(elementType, portName); + return { role, name: getElementPinName(node, portName) }; + }); + }; + // Detect standalone port nodes that become top-level layout ports. const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode'); // Detect built-in port or anchor nodes for element YAML export. @@ -658,8 +765,13 @@ const portNumber = normalizePortNumber(data && data.portNumber); const pitch = normalizePitch(data && data.pitch); const handleClearance = Math.max(pitch, 14); + const portDisplayName = String((data && (data.portName || data.componentDisplayName || data.label)) || 'port'); + const portWidth = Math.max( + PORT_NODE_SIZE, + PORT_LABEL_HORIZONTAL_PADDING + Math.max(PORT_LABEL_MIN_CHARS, portDisplayName.length) * PORT_LABEL_CHAR_WIDTH + ); return { - width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : PORT_NODE_SIZE, + width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : portWidth, height: Math.max(PORT_NODE_SIZE, PORT_NODE_SIZE + Math.max(0, portNumber - 1) * handleClearance) }; }; @@ -695,7 +807,7 @@ if (portNumber > 1) { const entries = []; Array.from({ length: portNumber }, (_, index) => { - const y = -PORT_NODE_SIZE / 2 + elementPortOffset(index, portNumber, pitch); + const y = elementPortOffset(index, portNumber, pitch); entries.push([`a${index + 1}`, { x: 0, y, a: 180, width }]); entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]); }); @@ -771,40 +883,41 @@ }; }; - // Flip an internal standalone Port angle into the outward-facing cell port - // angle used when this canvas is placed as a component elsewhere. + // Export standalone Port pins as the outward-facing pin angle. const externalPortAngle = (angle) => normalizeAngle(Number(angle ?? 0) + 180); - // Convert standalone port nodes into page-level layout ports. - const buildPageComponentPorts = (port, nodes) => { + // Convert standalone port nodes into page-level layout pins. + const buildPageComponentPins = (port, nodes) => { const portNodes = (nodes || []).filter(isPortElementNode); if (portNodes.length > 0) { - return portNodes.reduce((ports, node) => { + return portNodes.reduce((pins, node) => { const data = node.data || {}; const baseName = getNodePortName(node); const elementPorts = buildElementPorts('port', data); const entries = Object.entries(elementPorts); entries.forEach(([portName, portInfo]) => { - const exportName = entries.length === 1 - ? baseName - : `${baseName}_${portName.replace(/^port_/, '')}`; + const exportName = getElementPinName(node, portName); const point = getNodePortCanvasPoint(node, portName) || { x: Number((node.position && node.position.x) || 0), y: Number((node.position && node.position.y) || 0) }; - ports[exportName] = { + pins[exportName] = { + element: baseName, + pin: pinRoleFromElementPortName('port', portName), x: Number(point.x || 0), y: Number(point.y || 0), a: externalPortAngle(portInfo.a ?? data.angle ?? data.a ?? 0), width: Number(portInfo.width || data.width || 0.5) }; }); - return ports; + return pins; }, {}); } if (!port) return {}; return { - port: { + port_io1: { + element: 'port', + pin: 'io1', x: Number(port.x || 0), y: Number(port.y || 0), a: externalPortAngle(port.a || 0), @@ -813,27 +926,34 @@ }; }; - // Serialize standalone canvas ports into a layout ports YAML section. - const buildCanvasPortsYaml = (nodes, fallbackPort) => { - const ports = buildPageComponentPorts(fallbackPort, nodes); - const entries = Object.entries(ports); - if (entries.length === 0) return 'ports: []'; + // Backward-compatible helper name for callers that still use the old JS API. + const buildPageComponentPorts = buildPageComponentPins; + + // Serialize standalone canvas pins into a layout pins YAML section. + const buildCanvasPinsYaml = (nodes, fallbackPort) => { + const pins = buildPageComponentPins(fallbackPort, nodes); + const entries = Object.entries(pins); + if (entries.length === 0) return 'pins: []'; const sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node])); const lines = entries.map(([name, info]) => { - const data = (sourceNodes.get(name) && sourceNodes.get(name).data) || {}; + const data = (sourceNodes.get(info.element) && sourceNodes.get(info.element).data) || {}; const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : ''; return `- name: ${name} ${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'} + element: ${info.element} + pin: ${info.pin} x: ${Number(info.x || 0).toFixed(1)} y: ${canvasToLayoutY(info.y).toFixed(1)} angle: ${Number(info.a || 0).toFixed(1)} width: ${Number(info.width || 0.5)}${description}`; }); - return `ports:\n${lines.join('\n')}`; + return `pins:\n${lines.join('\n')}`; }; + const buildCanvasPortsYaml = buildCanvasPinsYaml; + // Maintain legacy single-port YAML export behavior for older callers. - const buildPortsYaml = (port) => buildCanvasPortsYaml([], port); + const buildPortsYaml = (port) => buildCanvasPinsYaml([], port); // Serialize built-in port and anchor nodes into layout element metadata. const buildElementsYaml = (nodes) => { @@ -845,16 +965,21 @@ const angle = data.elementType === 'port' ? data.angle : data.rotation; const portNumber = normalizePortNumber(data.portNumber); const pitch = normalizePitch(data.pitch); + const pinLines = buildElementPinEntries(node) + .map(pin => ` - name: ${pin.name}\n role: ${pin.role}`) + .join('\n'); return ` ${name}: type: ${data.elementType} x: ${Number((node.position && node.position.x) || 0).toFixed(1)} y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)} angle: ${Number(angle || 0).toFixed(1)} - port_number: ${portNumber} + pin_number: ${portNumber} pitch: ${Number(pitch)} layer: ${data.layer || 'WG_CORE'} width: ${Number(data.width || 0.5)} - description: ${toYamlScalar(data.description || '')}`; + description: ${toYamlScalar(data.description || '')} + pins: +${pinLines}`; }); return `elements:\n${lines.join('\n')}`; }; @@ -872,8 +997,12 @@ const targetNode = nodeMap[edge.target]; const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source; const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target; - const fromPort = edge.sourceHandle || 'unknown'; - const toPort = edge.targetHandle || 'unknown'; + const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType + ? getElementPinName(sourceNode, edge.sourceHandle) + : edge.sourceHandle || 'unknown'; + const toPort = targetNode && targetNode.data && targetNode.data.elementType + ? getElementPinName(targetNode, edge.targetHandle) + : edge.targetHandle || 'unknown'; const route = createRouteSettings(manifest, edge.data && edge.data.route); const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : []; const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : []; @@ -926,7 +1055,12 @@ ${linksYaml}`; const ports = buildElementPorts('port', node.data); const portInfo = ports && portName ? ports[portName] : ports.port; if (!portInfo) return { x: roundMeasureValue(x), y: roundMeasureValue(y) }; - const transformedInfo = transformPortInfo(portInfo, { rotation: 0 }); + const data = node.data || {}; + const transformedInfo = transformPortInfo(portInfo, { + rotation: data.angle ?? data.a ?? data.rotation ?? 0, + flip: Boolean(data.flip), + flop: Boolean(data.flop) + }); return { x: roundMeasureValue(x + Number(transformedInfo.x || 0)), y: roundMeasureValue(y - Number(transformedInfo.y || 0)) @@ -1127,12 +1261,16 @@ ${linksYaml}`; getNodePortCanvasPoint, buildPortHandles, buildElementPorts, + buildElementPinEntries, + getElementPinName, buildElementBoxSize, buildBasicComponentPorts, getBasicComponentMetadata, buildInstanceYaml, buildInstancesYaml, buildPageComponentPorts, + buildPageComponentPins, + buildCanvasPinsYaml, buildCanvasPortsYaml, buildBundlesYaml, buildPortsYaml, diff --git a/frontend/canvas.html b/frontend/canvas.html index 26462de..7f87a9d 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -662,6 +662,18 @@ Organization : OptiHK Limited transform: translateY(-1px); } + .origin-select-btn { + border-color: rgba(45, 212, 191, 0.55); + color: var(--accent-green); + } + + .origin-select-btn.active { + background: rgba(45, 212, 191, 0.16); + border-color: var(--accent-green); + color: var(--text-main); + box-shadow: 0 0 0 1px rgba(45, 212, 191, 0.2), 0 10px 20px rgba(45, 212, 191, 0.12); + } + body.light-mode .mini-btn { background: var(--mini-button-bg); border-color: rgba(30, 48, 69, 0.18); @@ -697,7 +709,10 @@ Organization : OptiHK Limited z-index: 10; display: flex; align-items: center; + flex-wrap: wrap; + justify-content: flex-end; gap: 10px; + max-width: calc(100% - 30px); padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; @@ -723,6 +738,71 @@ Organization : OptiHK Limited color: #102033; } + .coordinate-readout { + position: absolute; + left: 50%; + bottom: 18px; + transform: translateX(-50%); + z-index: 12; + display: flex; + align-items: center; + gap: 10px; + min-width: 270px; + justify-content: center; + padding: 7px 10px; + border: 1px solid var(--border); + border-radius: 7px; + background: rgba(13, 22, 38, 0.9); + color: var(--text-main); + box-shadow: 0 14px 28px var(--shadow); + backdrop-filter: blur(14px); + font: 600 0.62rem/1 'IBM Plex Mono', Consolas, Monaco, monospace; + pointer-events: none; + white-space: nowrap; + } + + .coordinate-readout span { + color: var(--text-muted); + font-weight: 500; + } + + body.light-mode .coordinate-readout { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(30, 48, 69, 0.16); + box-shadow: 0 14px 28px rgba(18, 32, 51, 0.12); + } + + .origin-crosshair { + position: absolute; + z-index: 18; + width: 22px; + height: 22px; + transform: translate(-50%, -50%); + pointer-events: none; + } + + .origin-crosshair::before, + .origin-crosshair::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + background: var(--accent-green); + box-shadow: 0 0 10px rgba(45, 212, 191, 0.35); + } + + .origin-crosshair::before { + width: 22px; + height: 1px; + transform: translate(-50%, -50%); + } + + .origin-crosshair::after { + width: 1px; + height: 22px; + transform: translate(-50%, -50%); + } + .build-layout-btn { position: absolute; bottom: 20px; @@ -1195,6 +1275,34 @@ Organization : OptiHK Limited pointer-events: none; } + .port-pin-label { + position: absolute; + z-index: 12; + pointer-events: none; + font-family: 'IBM Plex Mono', Consolas, Monaco, monospace; + font-size: 0.34rem; + font-weight: 700; + line-height: 1; + color: var(--port-label-text); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); + white-space: nowrap; + } + + .port-pin-label span { + display: block; + } + + .anchor-node-shell { + position: relative; + font-family: 'IBM Plex Sans', sans-serif; + } + + .anchor-visual-body { + position: relative; + box-sizing: border-box; + transform-origin: center center; + } + .build-progress { position: absolute; left: 50%; @@ -1459,11 +1567,12 @@ Organization : OptiHK Limited calculateCompositeBoxSize, buildPortHandles, buildElementPorts, + getElementPinName, buildElementBoxSize, getBasicComponentMetadata, buildInstancesYaml, buildPageComponentPorts, - buildCanvasPortsYaml, + buildCanvasPinsYaml, buildElementsYaml, buildBundlesYaml: buildRouteBundlesYaml, normalizeAngle, @@ -1599,9 +1708,9 @@ Organization : OptiHK Limited }, [id, data.ports, data.componentName, data.boxSize]); const baseHandleStyle = { - width: 8, height: 8, + width: 6, height: 6, background: 'var(--bg-main)', - border: '2px solid var(--accent)', + border: '1px solid var(--accent)', borderRadius: '50%', }; const handlePositionMap = { @@ -1610,27 +1719,32 @@ Organization : OptiHK Limited top: Position.Top, bottom: Position.Bottom }; - const portHandles = useMemo( - () => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }), - [data.ports, data.rotation, data.flip, data.flop] - ); const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE); + const portHandles = useMemo( + () => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }), + [data.ports, data.rotation, data.flip, data.flop, componentSize] + ); + const portDirectionMap = useMemo( + () => new Map(portHandles.map(handle => [handle.name, handle.position])), + [portHandles] + ); const isAnchorElement = data.elementType === 'anchor'; const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName); const visualSize = isAnchorElement ? { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE } : componentSize; + const componentVisualTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`; const iconSize = createComponentSymbolMetrics(componentSize); const portLabelStyle = (portHandle) => { const base = { ...portHandle.style }; if (portHandle.position === 'left') { - return { ...base, right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' }; + return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' }; } if (portHandle.position === 'right') { - return { ...base, left: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'left' }; + return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: 'translateY(-50%)', textAlign: 'left' }; } if (portHandle.position === 'top') { - return { ...base, bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' }; + return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' }; } - return { ...base, top: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' }; + return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: 'translateX(-50%)', textAlign: 'center' }; }; return ( @@ -1651,7 +1765,7 @@ Organization : OptiHK Limited height: visualSize.height, minHeight: visualSize.height, border: selected ? '2px solid var(--accent)' : '1px solid var(--border)', - transform: `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`, + transform: componentVisualTransform, boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)', ...(isBasicCompactComponent ? { padding: 0, @@ -1684,22 +1798,34 @@ Organization : OptiHK Limited )} +
+ {portHandles.map((portHandle) => ( + + + + + ))} +
+ {portHandles.map((portHandle) => ( - - - + {portHandle.name} @@ -1727,16 +1853,34 @@ Organization : OptiHK Limited // Renders standalone exported port elements with repeated port handles. + const pinLabelFromPortName = (portName) => { + const name = String(portName || ''); + const portMatch = name.match(/^port_(\d+)$/); + if (portMatch) return portMatch[1]; + if (name === 'port') return '1'; + return name; + }; + const PortNode = ({ id, data, selected }) => { const angle = data.angle ?? 0; + const canvasAngle = -Number(angle || 0); + const portDisplayName = data.portName || data.componentDisplayName || data.label || 'port'; const ports = buildElementPorts('port', data); const elementSize = buildElementBoxSize(data); const localHandlePorts = Object.fromEntries( Object.entries(ports).map(([name, info]) => [name, { ...info, a: 0 }]) ); + const localPortHandles = useMemo( + () => buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize }), + [localHandlePorts, elementSize] + ); const portHandles = useMemo( - () => buildPortHandles(localHandlePorts, { rotation: 0 }), - [localHandlePorts] + () => buildPortHandles(localHandlePorts, { rotation: canvasAngle }), + [localHandlePorts, canvasAngle] + ); + const portDirectionMap = useMemo( + () => new Map(portHandles.map(handle => [handle.name, handle.position])), + [portHandles] ); const handlePositionMap = { left: Position.Left, @@ -1746,27 +1890,61 @@ Organization : OptiHK Limited }; const baseHandleStyle = { background: 'var(--accent)', - width: 6, - height: 6 + width: 5, + height: 5 + }; + const pinLabelStyle = (portHandle) => { + const base = { + left: portHandle.style?.left, + right: portHandle.style?.right, + top: portHandle.style?.top, + bottom: portHandle.style?.bottom + }; + if (portHandle.position === 'left') return { ...base, transform: 'translate(calc(-100% - 5px), -50%)' }; + if (portHandle.position === 'right') return { ...base, transform: 'translate(5px, -50%)' }; + if (portHandle.position === 'top') return { ...base, transform: 'translate(-50%, calc(-100% - 5px))' }; + return { ...base, transform: 'translate(-50%, 5px)' }; + }; + const pinLabelTextStyle = { + transform: `rotate(${-canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})` }; return ( -
- P - {portHandles.map((portHandle) => ( - - - - - ))} +
+
+ {portDisplayName} + Port +
+
+ {localPortHandles.map((portHandle) => ( + + + +
+ {pinLabelFromPortName(portHandle.name)} +
+
+ ))} +
); }; @@ -1776,17 +1954,18 @@ Organization : OptiHK Limited const updateNodeInternals = useUpdateNodeInternals(); const anchorRotation = data.rotation || 0; const anchorVisualRotation = -Number(anchorRotation || 0); + const anchorDisplayName = data.componentDisplayName || data.label || 'anchor'; const ports = buildElementPorts('anchor', data); const elementSize = buildElementBoxSize(data); const localAnchorHandlePorts = Object.fromEntries( Object.entries(ports).map(([name, info]) => [name, { ...info, a: name.startsWith('a') || name.startsWith('left') ? 180 : 0 }]) ); const portHandles = useMemo( - () => buildPortHandles(localAnchorHandlePorts, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }), - [localAnchorHandlePorts, data.flip, data.flop] + () => buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize, flip: Boolean(data.flip), flop: Boolean(data.flop) }), + [localAnchorHandlePorts, elementSize, data.flip, data.flop] ); const anchorDirectionHandles = useMemo( - () => new Map(buildPortHandles(localAnchorHandlePorts, { rotation: Number(anchorRotation || 0), flip: Boolean(data.flip), flop: Boolean(data.flop) }).map(handle => [handle.name, handle.position])), + () => new Map(buildPortHandles(localAnchorHandlePorts, { rotation: -Number(anchorRotation || 0), flip: Boolean(data.flip), flop: Boolean(data.flop) }).map(handle => [handle.name, handle.position])), [localAnchorHandlePorts, anchorRotation, data.flip, data.flop] ); const handlePositionMap = { @@ -1796,8 +1975,8 @@ Organization : OptiHK Limited bottom: Position.Bottom }; const baseHandleStyle = { - width: 6, - height: 6, + width: 5, + height: 5, background: 'var(--accent)', border: '1px solid var(--bg-main)', borderRadius: '50%' @@ -1806,18 +1985,10 @@ Organization : OptiHK Limited const name = String(portName || ''); return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right'; }; - const anchorPortVisualTop = (portName) => { - const match = String(portName || '').match(/(\d+)$/); - const index = match ? Math.max(1, Number(match[1])) : 1; - const portCount = Math.max(1, Math.floor(Number(data.portNumber || 1))); - if (portCount <= 1) return elementSize.height / 2; - const travel = Math.max(0, elementSize.height - baseHandleStyle.height); - return baseHandleStyle.height / 2 + ((index - 1) / (portCount - 1)) * travel; - }; const anchorHandleVisualStyle = (portHandle, zIndex) => { const visualSide = anchorPortVisualSide(portHandle.name); const localLeft = visualSide === 'left' ? 0 : elementSize.width; - const localTop = anchorPortVisualTop(portHandle.name); + const localTop = portHandle.style?.top || '50%'; return { ...baseHandleStyle, zIndex, @@ -1828,47 +1999,66 @@ Organization : OptiHK Limited transform: 'translate(-50%, -50%)' }; }; + const pinLabelStyle = (portHandle) => { + const visualSide = anchorPortVisualSide(portHandle.name); + const localLeft = visualSide === 'left' ? 0 : elementSize.width; + const localTop = portHandle.style?.top || '50%'; + return { + left: localLeft, + top: localTop, + right: 'auto', + bottom: 'auto', + transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)' + }; + }; + const pinLabelTextStyle = { + transform: `rotate(${Number(anchorRotation || 0)}deg)` + }; useEffect(() => { updateNodeInternals(id); }, [id, data.ports, data.rotation, data.flip, data.flop, updateNodeInternals]); return ( -
- A - {portHandles.map((portHandle) => ( - - - - - ))} +
+
+ {anchorDisplayName} + Anchor +
+
+ {portHandles.map((portHandle) => ( + + + +
+ {pinLabelFromPortName(portHandle.name)} +
+
+ ))} +
); }); @@ -2919,7 +3109,7 @@ Organization : OptiHK Limited onUpdateNode(selectedNode.id, { data: { basicArguments: nextArguments, - ports: metadata?.ports || {}, + ports: metadata?.pins || metadata?.ports || {}, boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize } }); @@ -3558,6 +3748,10 @@ Organization : OptiHK Limited const [rulerStartPoint, setRulerStartPoint] = useState(null); const [rulerEndPoint, setRulerEndPoint] = useState(null); const [rulerPreviewPoint, setRulerPreviewPoint] = useState(null); + const [mouseCanvasPoint, setMouseCanvasPoint] = useState(null); + const [mouseScreenPoint, setMouseScreenPoint] = useState(null); + const [canvasOrigin, setCanvasOrigin] = useState({ x: 0, y: 0 }); + const [originPickMode, setOriginPickMode] = useState(false); const [projectTechnology, setProjectTechnology] = useState(''); const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST); const [currentLinkXsection, setCurrentLinkXsection] = useState('strip'); @@ -3565,6 +3759,7 @@ Organization : OptiHK Limited const [clipboard, setClipboard] = useState({ nodes: [] }); const initializedRef = useRef(false); + const canvasViewportRef = useRef(null); const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []); const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]); @@ -3602,6 +3797,14 @@ Organization : OptiHK Limited [rulerStartPoint, rulerActiveEndPoint] ); const rulerPreviewMeasurement = !rulerEndPoint && rulerPreviewPoint ? rulerMeasurement : null; + const displayMousePoint = useMemo(() => ( + mouseCanvasPoint + ? { + x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)), + y: Number((mouseCanvasPoint.y - canvasOrigin.y).toFixed(3)) + } + : null + ), [mouseCanvasPoint, canvasOrigin]); // Normalizes free-route control points and removes adjacent duplicates before storage. const compactRoutePoints = useCallback((points) => { return (points || []) @@ -3743,7 +3946,7 @@ Organization : OptiHK Limited const getAnchorHandleRouteDirection = useCallback((node, handleId) => { if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null; const handles = buildPortHandles(buildElementPorts('anchor', node.data), { - rotation: Number(node.data?.rotation || 0), + rotation: -Number(node.data?.rotation || 0), flip: Boolean(node.data?.flip), flop: Boolean(node.data?.flop) }); @@ -4414,10 +4617,7 @@ Organization : OptiHK Limited if (!element || typeof element !== 'object') return; const elementType = element.type === 'anchor' ? 'anchor' : (element.type === 'port' ? 'port' : ''); if (!elementType) return; - if (elementType === 'port' && elementName === 'port' && Array.isArray(doc.ports) && doc.ports.length > 0) { - return; - } - const portNumberValue = Math.floor(Number(element.port_number ?? element.portNumber ?? 1)); + const portNumberValue = Math.floor(Number(element.pin_number ?? element.pinNumber ?? element.port_number ?? element.portNumber ?? 1)); const portNumber = Number.isFinite(portNumberValue) ? Math.max(1, portNumberValue) : 1; const pitchValue = Number(element.pitch ?? DEFAULT_ELEMENT_PITCH); const pitch = Number.isFinite(pitchValue) ? Math.max(0, pitchValue) : DEFAULT_ELEMENT_PITCH; @@ -4436,6 +4636,7 @@ Organization : OptiHK Limited pitch, layer: element.layer || 'WG_CORE', description: element.description || '', + pinNames: Object.fromEntries((element.pins || []).map(pin => [pin.role, pin.name]).filter(([role, name]) => role && name)), boxSize: buildElementBoxSize({ elementType, portNumber, pitch }) }; const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; @@ -4479,6 +4680,14 @@ Organization : OptiHK Limited return nodes; }, []); + const resolveLoadedPinHandle = useCallback((node, pinName) => { + if (!node || !node.data?.elementType) return pinName; + const elementType = node.data.elementType === 'anchor' ? 'anchor' : 'port'; + const ports = buildElementPorts(elementType, node.data); + const matched = Object.keys(ports || {}).find(portName => getElementPinName(node, portName) === pinName); + return matched || pinName; + }, []); + useEffect(() => { const input = document.getElementById('open-yaml-input'); if (!input) return; @@ -4547,7 +4756,7 @@ Organization : OptiHK Limited componentDisplayName: instName, type: isProject ? 'composite' : undefined, availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents, - ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined), + ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined), boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined, forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined, basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined, @@ -4569,13 +4778,17 @@ Organization : OptiHK Limited const sourceId = nodeNameMap[fromInst]; const targetId = nodeNameMap[toInst]; if (sourceId && targetId) { + const sourceNode = newNodes.find(node => node.id === sourceId); + const targetNode = newNodes.find(node => node.id === targetId); + const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort); + const targetHandle = resolveLoadedPinHandle(targetNode, toPort); const view = routeStyleForSettings(route, false); newEdges.push({ - id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`, + id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`, source: sourceId, target: targetId, - sourceHandle: fromPort, - targetHandle: toPort, + sourceHandle, + targetHandle, type: view.type, style: view.style, data: { route, points: routePoints }, @@ -4591,8 +4804,8 @@ Organization : OptiHK Limited const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5); const newPageName = file.name.replace(/\.(yaml|yml)$/i, ''); - const importedPort = Array.isArray(doc.ports) && doc.ports[0] - ? { x: Number(doc.ports[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.ports[0].y) : Number(doc.ports[0].y || 0), a: Number(doc.ports[0].angle ?? doc.ports[0].a ?? 0), width: Number(doc.ports[0].width || 0.5) } + const importedPin = Array.isArray(doc.pins) && doc.pins[0] + ? { x: Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) } : { x: 50, y: 150, a: 0, width: 0.5 }; const newPage = { id: newPageId, @@ -4603,8 +4816,8 @@ Organization : OptiHK Limited { id: 'page-port', type: 'portNode', - position: { x: importedPort.x, y: importedPort.y }, - data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPort.a, width: importedPort.width || 0.5, layer: 'WG_CORE', description: '' }, + position: { x: importedPin.x, y: importedPin.y }, + data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPin.a, width: importedPin.width || 0.5, layer: 'WG_CORE', description: '' }, draggable: true, selectable: true, deletable: false, @@ -4612,7 +4825,7 @@ Organization : OptiHK Limited ...newNodes, ], edges: newEdges, - port: importedPort, + port: importedPin, }; setPages(prev => [...prev, newPage]); @@ -4663,7 +4876,7 @@ Organization : OptiHK Limited input.addEventListener('change', handleFile); return () => input.removeEventListener('change', handleFile); - }, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]); + }, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]); useEffect(() => { setProjectCompositeMap(prev => { @@ -4713,9 +4926,9 @@ Organization : OptiHK Limited const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => { const doc = jsyaml.load(content) || {}; const usesGdsYUp = doc.coordinate_system === 'gds_y_up'; - const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null; - const pagePort = firstPort - ? { x: Number(firstPort.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPort.y) : Number(firstPort.y || 0), a: Number(firstPort.angle ?? firstPort.a ?? 0), width: Number(firstPort.width || 0.5) } + const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null; + const pagePort = firstPin + ? { x: Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) } : { x: 50, y: 150, a: 0, width: 0.5 }; const nodeNameMap = {}; const nodes = [ @@ -4760,7 +4973,7 @@ Organization : OptiHK Limited componentDisplayName: instName, type: instIsComposite ? 'composite' : undefined, availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents, - ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined), + ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined), boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined, forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined, basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined, @@ -4781,13 +4994,17 @@ Organization : OptiHK Limited const sourceId = nodeNameMap[fromInst]; const targetId = nodeNameMap[toInst]; if (!sourceId || !targetId) return; + const sourceNode = nodes.find(node => node.id === sourceId); + const targetNode = nodes.find(node => node.id === targetId); + const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort); + const targetHandle = resolveLoadedPinHandle(targetNode, toPort); const view = routeStyleForSettings(route, false); edges.push({ - id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`, + id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`, source: sourceId, target: targetId, - sourceHandle: fromPort, - targetHandle: toPort, + sourceHandle, + targetHandle, type: view.type, style: view.style, data: { route, points: routePoints }, @@ -4861,7 +5078,7 @@ Organization : OptiHK Limited }; loadProject(); - }, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]); + }, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]); useEffect(() => { if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) { @@ -4949,7 +5166,7 @@ Organization : OptiHK Limited position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, boxSize), data: { ...node.data, - ports: metadata.ports || {}, + ports: metadata.pins || metadata.ports || {}, boxSize, foundry: metadata.foundry || '', process: metadata.process || '' @@ -5273,7 +5490,7 @@ Organization : OptiHK Limited type: 'composite', category: null, rotation: 0, - ports: parsedData.ports || {}, + ports: parsedData.pins || parsedData.ports || {}, boxSize: compositeBoxSize } }; @@ -5324,7 +5541,7 @@ Organization : OptiHK Limited type: 'composite', category: null, rotation: 0, - ports: parsedData.ports || {}, + ports: parsedData.pins || parsedData.ports || {}, boxSize: compositeBoxSize } }; @@ -5377,7 +5594,7 @@ Organization : OptiHK Limited libraryCategory: 'basic', category: 'basic', rotation: 0, - ports: metadata?.ports || {}, + ports: metadata?.pins || metadata?.ports || {}, boxSize: metadata ? normalizeBoxSize(metadata) : DEFAULT_COMPONENT_BOX_SIZE, basicArguments }, @@ -5585,13 +5802,16 @@ Organization : OptiHK Limited setRulerStartPoint(null); setRulerEndPoint(null); setRulerPreviewPoint(null); + } else { + setOriginPickMode(false); } return next; }); }, []); - // Convert a pane click or pointer event into canvas ruler coordinates. - const eventToRulerPoint = useCallback((event) => { + // Convert a pane click or pointer event into canvas coordinates. + const eventToCanvasPoint = useCallback((event) => { + if (!reactFlowInstance || !event) return null; const rawPoint = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }); return { x: Number(Math.min(activeCanvasSize.width, Math.max(0, rawPoint.x)).toFixed(3)), @@ -5599,12 +5819,41 @@ Organization : OptiHK Limited }; }, [reactFlowInstance, activeCanvasSize.width, activeCanvasSize.height]); + const updateMouseCanvasPoint = useCallback((event) => { + if (!activePage || activePage.type === 'layoutPreview') return null; + const nextPoint = eventToCanvasPoint(event); + if (!nextPoint) return null; + setMouseCanvasPoint(nextPoint); + const rect = canvasViewportRef.current?.getBoundingClientRect(); + if (rect) { + setMouseScreenPoint({ + x: event.clientX - rect.left, + y: event.clientY - rect.top + }); + } + return nextPoint; + }, [activePage, eventToCanvasPoint]); + + const toggleOriginPickMode = useCallback(() => { + setOriginPickMode(prev => { + const next = !prev; + if (next) { + setRulerMode(false); + setRulerStartPoint(null); + setRulerEndPoint(null); + setRulerPreviewPoint(null); + } + return next; + }); + }, []); + // Set ruler start/end points from canvas clicks. const handleRulerPaneClick = useCallback((event) => { if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return; event.preventDefault(); event.stopPropagation(); - const nextPoint = eventToRulerPoint(event); + const nextPoint = eventToCanvasPoint(event); + if (!nextPoint) return; if (!rulerStartPoint || rulerEndPoint) { setRulerStartPoint(nextPoint); setRulerEndPoint(null); @@ -5618,14 +5867,47 @@ Organization : OptiHK Limited if (measurement) { addLog(`Ruler distance: ${measurement.label}`); } - }, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint, addLog]); + }, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint, addLog]); // Update the live ruler preview point while measuring. const handleRulerMouseMove = useCallback((event) => { if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return; if (!rulerStartPoint || rulerEndPoint) return; - setRulerPreviewPoint(eventToRulerPoint(event)); - }, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint]); + const nextPoint = eventToCanvasPoint(event); + if (nextPoint) setRulerPreviewPoint(nextPoint); + }, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint]); + + const chooseCanvasOriginFromEvent = useCallback((event) => { + if (!originPickMode || !activePage || activePage.type === 'layoutPreview') return false; + const nextPoint = updateMouseCanvasPoint(event) || eventToCanvasPoint(event); + if (!nextPoint) return false; + event.preventDefault(); + event.stopPropagation(); + setCanvasOrigin(nextPoint); + setOriginPickMode(false); + addLog(`Canvas origin: (${nextPoint.x.toFixed(3)}, ${nextPoint.y.toFixed(3)}) um`); + return true; + }, [originPickMode, activePage, updateMouseCanvasPoint, eventToCanvasPoint, addLog]); + + const handleCanvasMouseMove = useCallback((event) => { + updateMouseCanvasPoint(event); + handleRulerMouseMove(event); + }, [updateMouseCanvasPoint, handleRulerMouseMove]); + + const handleCanvasPaneClick = useCallback((event) => { + if (chooseCanvasOriginFromEvent(event)) return; + handleRulerPaneClick(event); + }, [chooseCanvasOriginFromEvent, handleRulerPaneClick]); + + const handleCanvasNodeClick = useCallback((event) => { + if (chooseCanvasOriginFromEvent(event)) return; + handleRulerPaneClick(event); + }, [chooseCanvasOriginFromEvent, handleRulerPaneClick]); + + const handleCanvasMouseLeave = useCallback(() => { + setMouseCanvasPoint(null); + setMouseScreenPoint(null); + }, []); // Select a route edge by id with optional additive selection. const selectEdgeById = useCallback((edgeId, additive = false) => { @@ -5873,7 +6155,7 @@ type: ${page.type === 'project' ? 'project' : 'composite'} version: "1.0.0" # 1. External Ports (How this cell connects to the outside world) -${buildCanvasPortsYaml(page.nodes)} +${buildCanvasPinsYaml(page.nodes)} # 2. Instances (The sub-components dropped onto this canvas) instances:`; @@ -6098,8 +6380,11 @@ ${bundlesBlock}`; ))}
Snap to Grid @@ -6154,6 +6439,14 @@ ${bundlesBlock}`; +
{buildProgress.active && ( @@ -6176,6 +6469,25 @@ ${bundlesBlock}`;
)} + {activePage && activePage.type !== 'layoutPreview' && ( +
+ X {displayMousePoint ? displayMousePoint.x.toFixed(3) : '--'} um + Y {displayMousePoint ? displayMousePoint.y.toFixed(3) : '--'} um + O {canvasOrigin.x.toFixed(3)}, {canvasOrigin.y.toFixed(3)} +
+ )} + + {originPickMode && mouseScreenPoint && activePage?.type !== 'layoutPreview' && ( +
+ )} + {activePage && activePage.type !== 'layoutPreview' && (