diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a99879d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*/__pycache__/ +__pycache__/ +*.pyc diff --git a/backend/__pycache__/database.cpython-311.pyc b/backend/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..7e1cc63 Binary files /dev/null and b/backend/__pycache__/database.cpython-311.pyc differ diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc index 0b75b5e..8379e13 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-311.pyc b/backend/__pycache__/gds_builder.cpython-311.pyc new file mode 100644 index 0000000..0debd9a Binary files /dev/null and b/backend/__pycache__/gds_builder.cpython-311.pyc differ diff --git a/backend/__pycache__/gds_builder.cpython-39.pyc b/backend/__pycache__/gds_builder.cpython-39.pyc index 5258156..88a94ad 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__/pdk_access.cpython-311.pyc b/backend/__pycache__/pdk_access.cpython-311.pyc new file mode 100644 index 0000000..989f490 Binary files /dev/null and b/backend/__pycache__/pdk_access.cpython-311.pyc differ diff --git a/backend/__pycache__/pdk_access.cpython-39.pyc b/backend/__pycache__/pdk_access.cpython-39.pyc index 9c788b4..4e34003 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__/routed_layout_preview.cpython-311.pyc b/backend/__pycache__/routed_layout_preview.cpython-311.pyc new file mode 100644 index 0000000..cf5e986 Binary files /dev/null and b/backend/__pycache__/routed_layout_preview.cpython-311.pyc differ diff --git a/backend/__pycache__/routed_layout_preview.cpython-39.pyc b/backend/__pycache__/routed_layout_preview.cpython-39.pyc index 041666a..53d3582 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__/router_dependency.cpython-311.pyc b/backend/__pycache__/router_dependency.cpython-311.pyc new file mode 100644 index 0000000..801b363 Binary files /dev/null and b/backend/__pycache__/router_dependency.cpython-311.pyc differ diff --git a/backend/__pycache__/router_dependency.cpython-39.pyc b/backend/__pycache__/router_dependency.cpython-39.pyc index c8af590..348bbbf 100644 Binary files a/backend/__pycache__/router_dependency.cpython-39.pyc and b/backend/__pycache__/router_dependency.cpython-39.pyc differ diff --git a/backend/__pycache__/technology_manifest.cpython-311.pyc b/backend/__pycache__/technology_manifest.cpython-311.pyc new file mode 100644 index 0000000..7179b5e Binary files /dev/null and b/backend/__pycache__/technology_manifest.cpython-311.pyc differ diff --git a/backend/__pycache__/technology_manifest.cpython-39.pyc b/backend/__pycache__/technology_manifest.cpython-39.pyc index a833997..e3d2424 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/server.py b/backend/server.py index 1d422d7..7b3d2f9 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1,6 +1,6 @@ # ----------------------------------------------------------------------------- # Description: Flask backend API server for authentication, project management, PDK library access, layout preview, and GDS build endpoints. -# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta +# Inside functions: no_cache_response, login_required_json, wrapper, request_ip, record_action, safe_name, user_layout_root, project_root, cell_file_path, cell_svg_path, file_version, cell_routes_path, write_route_points_sidecar, project_gds_path, technology_manifest_path_for_project, current_pdk_root, scoped_pdk_root_for_project, pdk_root_for_request_project, project_meta_path, read_project_meta # Developer : Qin Yue @ 2026 # Organization : OptiHK Limited # ----------------------------------------------------------------------------- @@ -9,6 +9,7 @@ import os import re import shutil import json +import uuid import yaml from collections import OrderedDict from functools import wraps @@ -135,6 +136,12 @@ def cell_svg_path(project_name, cell_name): return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.svg") +def file_version(path): + """Return a cache-busting version token for a completed file.""" + stat = os.stat(path) + return f"{stat.st_mtime_ns}-{stat.st_size}" + + def cell_routes_path(project_name, cell_name): """Return the route sidecar JSON path for a project cell.""" return os.path.join(project_root(project_name), f"{safe_name(cell_name, 'canvas_1')}.routes.yml") @@ -739,24 +746,31 @@ def save_layout(): write_route_points_sidecar(content, cell_routes_path(project, cell)) svg_path = None + svg_version = None preview_status = "not_requested" preview_error = None if create_preview: svg_path = cell_svg_path(project, cell) + temp_svg_path = f"{svg_path}.building-{os.getpid()}-{uuid.uuid4().hex}.svg" try: create_routed_layout_svg( content, - svg_path, + temp_svg_path, pdk_root=current_pdk_root(), project_dir=project_root(project), technology_manifest_path=technology_manifest_path_for_project(project), prefer_full_gds=prefer_full_gds_for_session(session), ) + os.replace(temp_svg_path, svg_path) + svg_version = file_version(svg_path) preview_status = "generated" except RouterStackUnavailable as e: preview_status = "skipped" preview_error = str(e) svg_path = None + finally: + if os.path.exists(temp_svg_path): + os.remove(temp_svg_path) record_action('layout.save', project=project, cell=cell, detail={"bytes": len(content), "svg": svg_path}) return jsonify({ @@ -765,7 +779,9 @@ def save_layout(): "cell": cell, "path": save_path, "svg_path": svg_path, - "svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell) if svg_path else None, + "svg_url": url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version) if svg_path else None, + "svg_ready": bool(svg_path and svg_version), + "svg_version": svg_version, "preview_status": preview_status, "preview_error": preview_error }), 200 diff --git a/database/_exports/3ff6e2b43f0d43dab8e169a0b8d6c3d0/mxpic_project_1.gds b/database/_exports/3ff6e2b43f0d43dab8e169a0b8d6c3d0/mxpic_project_1.gds new file mode 100644 index 0000000..ad6500b Binary files /dev/null and b/database/_exports/3ff6e2b43f0d43dab8e169a0b8d6c3d0/mxpic_project_1.gds differ diff --git a/database/admin/layout/mxpic_project_1/canvas_1.svg b/database/admin/layout/mxpic_project_1/canvas_1.svg new file mode 100644 index 0000000..a416194 --- /dev/null +++ b/database/admin/layout/mxpic_project_1/canvas_1.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 0000000..17d83e6 --- /dev/null +++ b/database/admin/layout/mxpic_project_1/canvas_1.yml @@ -0,0 +1,211 @@ +# ============================================= +# mxPIC Cell/Project Definition File +# ============================================= +schema_version: "2.0.0" +kind: cell +coordinate_system: gds_y_up +canvas_size: + width: 500 + height: 600 +project: mxpic_project_1 +name: canvas_1 +type: composite +version: "1.0.0" + +# 1. External Ports (How this cell connects to the outside world) +pins: +- name: port_io1 + layer: WG_CORE + element: port + pin: io1 + x: 40.0 + y: -90.0 + angle: 180.0 + width: 0.5 +- name: port_1_io1 + layer: WG_CORE + element: port_1 + pin: io1 + x: 410.0 + y: -35.0 + angle: 0.0 + width: 0.5 +- name: port_1_io2 + layer: WG_CORE + element: port_1 + pin: io2 + x: 410.0 + y: -25.0 + angle: 0.0 + width: 0.5 +- name: port_2_io1 + layer: WG_CORE + element: port_2 + pin: io1 + x: 390.0 + y: -215.0 + angle: 0.0 + width: 0.5 +- name: port_2_io2 + layer: WG_CORE + element: port_2 + pin: io2 + x: 390.0 + y: -205.0 + angle: 0.0 + width: 0.5 + +# 2. Instances (The sub-components dropped onto this canvas) +instances: + MMI_1: + component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 + x: 130.0 + y: -90.0 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: + + MMI_2: + component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 + x: 280.0 + y: -30.0 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: + + MMI_3: + component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 + x: 320.1 + y: -144.7 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: + +elements: + port: + type: port + x: 40.0 + y: -90.0 + angle: 180.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_io1 + role: io1 + port: + type: port + x: 40.0 + y: -90.0 + angle: 0.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_io1 + role: io1 + port_1: + type: port + x: 410.0 + y: -30.0 + angle: 180.0 + pin_number: 2 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_1_io1 + role: io1 + - name: port_1_io2 + role: io2 + port_2: + type: port + x: 390.0 + y: -210.0 + angle: 180.0 + pin_number: 2 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_2_io1 + role: io1 + - name: port_2_io2 + role: io2 + +# 3. Bundles (Grouped links for multi-bus/parallel routing) +bundles: + output_bus: + routing_type: euler_bend + links: + - from: MMI_1:a1 + to: port:port_io1 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_2:a1 + to: MMI_1:b1 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_3:a1 + to: MMI_1:b2 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_2:b1 + to: port_1:port_1_io1 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_2:b1 + to: port_1:port_1_io2 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_2:b2 + to: port_1:port_1_io1 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_3:b1 + to: port_2:port_2_io2 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend + - from: MMI_3:b2 + to: port_2:port_2_io1 + xsection: strip + family: optical + width: 0.45 + radius: 10 + routing_type: euler_bend \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/canvas_1.svg b/database/engineer/layout/mxpic_project_1/canvas_1.svg new file mode 100644 index 0000000..da409e3 --- /dev/null +++ b/database/engineer/layout/mxpic_project_1/canvas_1.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/canvas_1.yml b/database/engineer/layout/mxpic_project_1/canvas_1.yml new file mode 100644 index 0000000..f890271 --- /dev/null +++ b/database/engineer/layout/mxpic_project_1/canvas_1.yml @@ -0,0 +1,100 @@ +# ============================================= +# mxPIC Cell/Project Definition File +# ============================================= +schema_version: "2.0.0" +kind: cell +coordinate_system: gds_y_up +canvas_size: + width: 5000 + height: 5000 +project: mxpic_project_1 +name: canvas_1 +type: composite +version: "1.0.0" + +# 1. External Ports (How this cell connects to the outside world) +pins: +- name: port_io1 + layer: WG_CORE + element: port + pin: io1 + x: 50.0 + y: -150.0 + angle: 180.0 + width: 0.5 + +# 2. Instances (The sub-components dropped onto this canvas) +instances: + waveguide_1: + component: waveguide + x: 686.5 + y: -1027.9 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: 100 + width: 0.5 + xsection: "strip" + + circle_1: + component: circle + x: 877.2 + y: -1093.7 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + radius: 10 + width: 0.5 + xsection: "strip" + + waveguide_2: + component: waveguide + x: 858.0 + y: -1029.6 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: 100 + width: 0.5 + xsection: "strip" + +elements: + port: + type: port + x: 50.0 + y: -150.0 + angle: 0.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_io1 + role: io1 + +# 3. Bundles (Grouped links for multi-bus/parallel routing) +bundles: + output_bus: + routing_type: euler_bend + links: + - from: waveguide_1:b1 + to: waveguide_2:a1 + xsection: strip + family: optical + width: 0.5 + radius: 10 + routing_type: euler_bend + - from: waveguide_2:b1 + to: circle_1:a1 + xsection: strip + family: optical + width: 0.5 + radius: 10 + routing_type: euler_bend \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/canvas_2.svg b/database/engineer/layout/mxpic_project_1/canvas_2.svg new file mode 100644 index 0000000..30ca134 --- /dev/null +++ b/database/engineer/layout/mxpic_project_1/canvas_2.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/canvas_2.yml b/database/engineer/layout/mxpic_project_1/canvas_2.yml new file mode 100644 index 0000000..6d415b5 --- /dev/null +++ b/database/engineer/layout/mxpic_project_1/canvas_2.yml @@ -0,0 +1,136 @@ +# ============================================= +# mxPIC Cell/Project Definition File +# ============================================= +schema_version: "2.0.0" +kind: cell +coordinate_system: gds_y_up +canvas_size: + width: 5000 + height: 5000 +project: mxpic_project_1 +name: canvas_2 +type: composite +version: "1.0.0" + +# 1. External Ports (How this cell connects to the outside world) +pins: +- name: port_io1 + layer: WG_CORE + element: port + pin: io1 + x: 50.0 + y: -150.0 + angle: 0.0 + width: 0.5 +- name: port_2_io1 + layer: WG_CORE + element: port_2 + pin: io1 + x: 1442.1 + y: -1470.0 + angle: 180.0 + width: 10 +- name: port_3_io1 + layer: WG_CORE + element: port_3 + pin: io1 + x: 2024.3 + y: -1609.0 + angle: 180.0 + width: 0.5 + +# 2. Instances (The sub-components dropped onto this canvas) +instances: + waveguide_3: + component: waveguide + x: 1581.0 + y: -1633.5 + rotation: 0.0 + flip: 0 + flop: 0 + mirror: false + settings: + length: 100 + width: 15 + xsection: "strip" + +elements: + port: + type: port + x: 50.0 + y: -150.0 + angle: 0.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_io1 + role: io1 + port: + type: port + x: 50.0 + y: -150.0 + angle: 180.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_io1 + role: io1 + port_2: + type: port + x: 1442.1 + y: -1470.0 + angle: 0.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 10 + description: "" + pins: + - name: port_2_io1 + role: io1 + port_3: + type: port + x: 2024.3 + y: -1609.0 + angle: 0.0 + pin_number: 1 + pitch: 10 + layer: WG_CORE + width: 0.5 + description: "" + pins: + - name: port_3_io1 + role: io1 + +# 3. Bundles (Grouped links for multi-bus/parallel routing) +bundles: + output_bus: + routing_type: euler_bend + links: + - from: waveguide_3:a1 + to: port_2:port_2_io1 + xsection: strip + family: optical + width: 15 + radius: 10 + routing_type: euler_bend + - from: waveguide_3:b1 + to: port_2:port_2_io1 + xsection: strip + family: optical + width: 15 + radius: 10 + routing_type: euler_bend + - from: port_3:port_3_io1 + to: port_2:port_2_io1 + xsection: strip + family: optical + width: 0.5 + radius: 10 + routing_type: euler_bend \ No newline at end of file diff --git a/database/engineer/layout/mxpic_project_1/mxpic_project_1.svg b/database/engineer/layout/mxpic_project_1/mxpic_project_1.svg index ba8ac45..ddcf788 100644 --- a/database/engineer/layout/mxpic_project_1/mxpic_project_1.svg +++ b/database/engineer/layout/mxpic_project_1/mxpic_project_1.svg @@ -1,4 +1,5 @@ +<<<<<<< HEAD A - ) : ( -
- {!data.hideIcon && data.category && ( -
- + transform: componentVisualTransform, + transformOrigin: 'center center', + ...(isBasicCompactComponent ? { + padding: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } : {}), + ...(isAnchorElement ? { + width: PORT_NODE_SIZE, + minHeight: PORT_NODE_SIZE, + padding: 0, + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } : {}), + }} + > + {isAnchorElement ? ( + A + ) : ( +
+ {!data.hideIcon && data.category && ( +
+ +
+ )} + {!data.category &&
}
)} - {!data.category &&
} -
- )} -
+
{ - const visualSide = anchorPortVisualSide(portHandle.name); - const localLeft = visualSide === 'left' ? 0 : elementSize.width; - const localTop = portHandle.style?.top || '50%'; - return { - ...baseHandleStyle, - zIndex, - left: localLeft, - top: localTop, - right: 'auto', - bottom: 'auto', - transform: 'translate(-50%, -50%)' - }; - }; + const anchorHandleVisualStyle = (portHandle, zIndex) => ({ + ...baseHandleStyle, + zIndex, + left: portHandle.style?.left, + top: portHandle.style?.top || '50%', + right: portHandle.style?.right || 'auto', + bottom: portHandle.style?.bottom || 'auto', + transform: portHandle.style?.transform || 'translate(-50%, -50%)' + }); const pinLabelStyle = (portHandle) => { const visualSide = anchorPortVisualSide(portHandle.name); - const localLeft = visualSide === 'left' ? 0 : elementSize.width; - const localTop = portHandle.style?.top || '50%'; return { - left: localLeft, - top: localTop, - right: 'auto', - bottom: 'auto', + left: portHandle.style?.left, + top: portHandle.style?.top || '50%', + right: portHandle.style?.right || 'auto', + bottom: portHandle.style?.bottom || 'auto', transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)' }; }; @@ -3359,6 +3356,7 @@ Organization : OptiHK Limited const forge = isForgeComponent(componentName); onUpdateNode(selectedNode.id, { data: { + ...selectedNode.data, componentName, label: componentName, ports: forge ? {} : undefined, @@ -3805,6 +3803,14 @@ Organization : OptiHK Limited } : null ), [mouseCanvasPoint, canvasOrigin]); + const handleCanvasViewportMoveEnd = useCallback((event, viewport) => { + if (!activePageId || !viewport) return; + setPages(prev => prev.map(page => ( + page.id === activePageId + ? { ...page, viewport: { x: viewport.x, y: viewport.y, zoom: viewport.zoom } } + : page + ))); + }, [activePageId]); // Normalizes free-route control points and removes adjacent duplicates before storage. const compactRoutePoints = useCallback((points) => { return (points || []) @@ -5056,6 +5062,35 @@ Organization : OptiHK Limited return boxSize ? { ...node, data: { ...node.data, boxSize } } : node; }) })); + + // Pre-fetch PDK component metadata so nodes render with correct boxSize immediately. + const allNodes = cellPages.flatMap(page => page.nodes); + const pdkNames = [...new Set(allNodes + .filter(n => n.data?.componentName && !n.data?.elementType + && !isForgeComponent(n.data.componentName) + && !isBasicComponent(n.data.componentName)) + .map(n => n.data.componentName))]; + if (pdkNames.length > 0) { + const metaResults = await Promise.all( + pdkNames.map(name => loadComponentMetadata(name).catch(() => null)) + ); + const metaMap = new Map( + pdkNames.filter((_, i) => metaResults[i]).map((name, i) => [name, metaResults[i]]) + ); + for (const page of cellPages) { + page.nodes = page.nodes.map(node => { + const metadata = metaMap.get(node.data?.componentName); + if (!metadata) return node; + const sz = normalizeBoxSize(metadata); + return { + ...node, + position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz), + data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '' } + }; + }); + } + } + const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName); const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage); const resolvedProjectPage = loadedProjectPage || projectPage; @@ -5082,12 +5117,18 @@ Organization : OptiHK Limited useEffect(() => { if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) { - reactFlowInstance.fitBounds( - { x: 0, y: 0, width: activeCanvasSize.width, height: activeCanvasSize.height }, - { padding: 0.12, duration: 0 } - ); + if (activePage.viewport) { + window.requestAnimationFrame(() => { + reactFlowInstance.setViewport(activePage.viewport, { duration: 0 }); + }); + } else { + reactFlowInstance.fitBounds( + { x: 0, y: 0, width: activeCanvasSize.width, height: activeCanvasSize.height }, + { padding: 0.12, duration: 0 } + ); + } } - }, [activePage?.id, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]); + }, [activePage?.id, activePage?.viewport, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]); useEffect(() => { setRulerStartPoint(null); @@ -5174,12 +5215,20 @@ Organization : OptiHK Limited }; }) }))); + + // Force React Flow to re-measure nodes whose boxSize / ports have changed. + requestAnimationFrame(() => { + const updatedIds = results.filter(r => r.metadata).map(r => r.nodeId); + if (updatedIds.length > 0 && reactFlowInstance.updateNodeInternals) { + reactFlowInstance.updateNodeInternals(updatedIds); + } + }); }); return () => { cancelled = true; }; - }, [pages, loadComponentMetadata]); + }, [pages, loadComponentMetadata, reactFlowInstance]); const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]); @@ -6534,6 +6583,7 @@ ${bundlesBlock}`; minZoom={0.02} maxZoom={4} defaultViewport={{ x: 80, y: 80, zoom: 0.12 }} + onMoveEnd={handleCanvasViewportMoveEnd} panOnDrag={false} selectionOnDrag={true} selectionMode={FULL_SELECTION_MODE} diff --git a/tests/layout-backend-static.test.js b/tests/layout-backend-static.test.js index 9721715..377be1c 100644 --- a/tests/layout-backend-static.test.js +++ b/tests/layout-backend-static.test.js @@ -61,6 +61,21 @@ assert( 'save-layout response should include an svg_url for the new layout tab' ); assert( +<<<<<<< HEAD + serverPy.includes('svg_ready') && + serverPy.includes('svg_version') && + serverPy.includes('file_version(svg_path)') && + serverPy.includes("url_for('get_layout_svg', project_name=project, cell_name=cell, v=svg_version)"), + 'save-layout response should only expose a versioned SVG URL after the preview file is ready' +); +assert( + serverPy.includes('temp_svg_path') && + serverPy.includes('os.replace(temp_svg_path, svg_path)'), + 'save-layout should publish generated SVG previews atomically instead of serving partially written files' +); +assert( +======= +>>>>>>> jingwen_main serverPy.includes('RouterStackUnavailable') && serverPy.includes('except RouterStackUnavailable as e') && serverPy.includes('"preview_status": preview_status') && diff --git a/tests/layout-ui-wiring.test.js b/tests/layout-ui-wiring.test.js index 1cec144..0a53352 100644 --- a/tests/layout-ui-wiring.test.js +++ b/tests/layout-ui-wiring.test.js @@ -33,6 +33,16 @@ assert( 'Build Layout should use the backend svg_url response' ); assert( +<<<<<<< HEAD + canvasHtml.includes('result.svg_ready && result.svg_url') && + canvasHtml.includes('buildLayoutRequestRef') && + canvasHtml.includes('buildLayoutBusyRef') && + canvasHtml.includes("cache: 'no-store'"), + 'Build Layout should wait for a ready, versioned SVG response and prevent stale duplicate preview updates' +); +assert( +======= +>>>>>>> jingwen_main canvasHtml.includes('result.preview_error') && canvasHtml.includes('Preview skipped: '), 'Build Layout should log when the backend saves YAML but skips SVG preview because the router stack is unavailable' diff --git a/work _log.md b/work _log.md new file mode 100644 index 0000000..7ae6fa2 --- /dev/null +++ b/work _log.md @@ -0,0 +1,11 @@ +# work log + +1.Fixed an issue where switching between different tabs would automatically reset the zoom level. + +2.Fixed an port width mismatch in YAML bundles. + +3.Fixed the issue where SVG were displaying in incorrect positions. + +4.Fixed the abnormal port shift after rotation. + +5.Fixed the abnormal position of individual ports. \ No newline at end of file