Updated with more bug revise. The login page and dashboard is also changedd

This commit is contained in:
2026-05-31 10:14:50 +08:00
parent e3f708a1a7
commit 9b4e8da796
14 changed files with 1986 additions and 2189 deletions
Binary file not shown.
-1
View File
@@ -9,7 +9,6 @@ from typing import Dict, Optional
import yaml
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str:
"""Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
layout = yaml.safe_load(yaml_content) or {}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 149 KiB

@@ -5,8 +5,8 @@ schema_version: "2.0.0"
kind: cell
coordinate_system: gds_y_up
canvas_size:
width: 1000
height: 1000
width: 5000
height: 500
project: mxpic_project_1
name: canvas_1
type: composite
@@ -16,35 +16,29 @@ version: "1.0.0"
ports:
- name: port
layer: WG_CORE
x: 200.0
y: -370.0
angle: 180.0
width: 0.5
- name: port_2
layer: WG_CORE
x: 200.0
y: -370.0
x: 103.5
y: -127.3
angle: 180.0
width: 0.5
- name: port_1
layer: WG_CORE
x: 200.0
y: -420.0
x: 108.7
y: -252.6
angle: 180.0
width: 0.5
- name: port_3
- name: port_2
layer: WG_CORE
x: 691.9
y: -267.5
x: 497.4
y: -131.6
angle: 0.0
width: 0.5
# 2. Instances (The sub-components dropped onto this canvas)
instances:
MMI_7:
MMI_1:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 310.0
y: -370.0
x: 177.9
y: -252.1
rotation: 0.0
flip: 0
flop: 0
@@ -52,54 +46,10 @@ instances:
settings:
length:
MMI_8:
MMI_2:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 560.0
y: -130.0
rotation: 0.0
flip: 0
flop: 0
mirror: false
settings:
length:
MMI_9:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 560.0
y: -180.0
rotation: 0.0
flip: 0
flop: 0
mirror: false
settings:
length:
MMI_10:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 560.0
y: -230.0
rotation: 0.0
flip: 0
flop: 0
mirror: false
settings:
length:
MMI_11:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 560.0
y: -280.0
rotation: 0.0
flip: 0
flop: 0
mirror: false
settings:
length:
MMI_12:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 310.0
y: -420.0
x: 356.7
y: -142.9
rotation: 0.0
flip: 0
flop: 0
@@ -110,28 +60,8 @@ instances:
elements:
port:
type: port
x: 200.0
y: -370.0
angle: 0.0
port_number: 1
pitch: 10
layer: WG_CORE
width: 0.5
description: ""
Anchor_1:
type: anchor
x: 460.0
y: -300.0
angle: 90.0
port_number: 4
pitch: 10
layer: WG_CORE
width: 0.5
description: ""
port_2:
type: port
x: 200.0
y: -370.0
x: 103.5
y: -127.3
angle: 0.0
port_number: 1
pitch: 10
@@ -140,18 +70,18 @@ elements:
description: ""
port_1:
type: port
x: 200.0
y: -420.0
x: 108.7
y: -252.6
angle: 0.0
port_number: 1
pitch: 10
layer: WG_CORE
width: 0.5
description: ""
port_3:
port_2:
type: port
x: 691.9
y: -267.5
x: 497.4
y: -131.6
angle: 180.0
port_number: 1
pitch: 10
@@ -164,78 +94,22 @@ bundles:
output_bus:
routing_type: euler_bend
links:
- from: MMI_7:a1
to: port_2:port
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_12:a1
- from: MMI_1:a1
to: port_1:port
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_7:b1
to: Anchor_1:a1
- from: MMI_1:b1
to: MMI_2:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_7:b2
to: Anchor_1:a2
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_12:b1
to: Anchor_1:a3
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_12:b2
to: Anchor_1:a4
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: Anchor_1:b4
to: MMI_11:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: Anchor_1:b3
to: MMI_10:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: Anchor_1:b2
to: MMI_9:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: Anchor_1:b1
to: MMI_8:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_11:b1
to: port_3:port
- from: MMI_2:b1
to: port_2:port
xsection: strip
family: optical
width: 0.45
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 195 KiB

@@ -23,10 +23,10 @@ ports:
# 2. Instances (The sub-components dropped onto this canvas)
instances:
MZM_1:
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZI_SiN400_Si220_PIN_mod_1310_L1300_QY_202603
x: 1740.0
y: -2350.0
canvas_1:
component: canvas_1
x: 476.9
y: -1056.4
rotation: 0.0
flip: 0
flop: 0
@@ -34,10 +34,10 @@ instances:
settings:
length:
canvas_1:
canvas_1_1:
component: canvas_1
x: 903.5
y: -2681.6
x: 1139.8
y: -958.5
rotation: 0.0
flip: 0
flop: 0
@@ -62,15 +62,8 @@ bundles:
output_bus:
routing_type: euler_bend
links:
- from: canvas_1:port_1
to: MZM_1:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: canvas_1:port_3
to: MZM_1:a2
- from: canvas_1_1:port_1
to: canvas_1:port_2
xsection: strip
family: optical
width: 0.45
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+48
View File
@@ -360,6 +360,53 @@
};
};
// Calculate the visual footprint of a user canvas when placed as a component.
const calculateCompositeBoxSize = (pageOrNodes, fallback) => {
const page = Array.isArray(pageOrNodes) ? { nodes: pageOrNodes } : (pageOrNodes || {});
const nodes = Array.isArray(page.nodes) ? page.nodes : [];
const fallbackSize = normalizeBoxSize({ box_size: fallback }, DEFAULT_COMPONENT_BOX_SIZE);
const points = [];
const addPoint = (x, y) => {
const px = Number(x);
const py = Number(y);
if (Number.isFinite(px) && Number.isFinite(py)) {
points.push({ x: px, y: py });
}
};
Object.values(buildPageComponentPorts(page.port, nodes)).forEach(port => {
addPoint(port.x, port.y);
});
nodes.forEach(node => {
if (!node || node.type !== 'rotatableNode' || node.data?.elementType || !node.position) return;
const box = normalizeBoxSize({ box_size: node.data?.boxSize }, fallbackSize);
const origin = {
x: Number(node.position.x) || 0,
y: Number(node.position.y) || 0
};
[
{ x: 0, y: 0 },
{ x: box.width, y: 0 },
{ x: box.width, y: box.height },
{ x: 0, y: box.height }
].forEach(corner => {
const transformed = transformBoxCorner(corner, node.data);
addPoint(origin.x + transformed.x, origin.y + transformed.y);
});
});
if (points.length < 2) return fallbackSize;
const minX = Math.min(...points.map(point => point.x));
const maxX = Math.max(...points.map(point => point.x));
const minY = Math.min(...points.map(point => point.y));
const maxY = Math.max(...points.map(point => point.y));
return {
width: Math.max(1, roundMeasureValue(maxX - minX)),
height: Math.max(1, roundMeasureValue(maxY - minY))
};
};
// Round ruler measurements for compact display.
const roundMeasureValue = (value) => Number(value.toFixed(3));
@@ -1073,6 +1120,7 @@ ${linksYaml}`;
normalizeCanvasSize,
clampPositionToCanvas,
calculateLayoutBounds,
calculateCompositeBoxSize,
createRulerMeasurement,
createComponentSymbolMetrics,
transformPortInfo,
+173 -86
View File
@@ -1456,6 +1456,7 @@ Organization : OptiHK Limited
normalizeCanvasSize,
clampPositionToCanvas,
calculateLayoutBounds,
calculateCompositeBoxSize,
buildPortHandles,
buildElementPorts,
buildElementBoxSize,
@@ -2245,7 +2246,7 @@ Organization : OptiHK Limited
}
const dragData = JSON.stringify(
isUserCell
? { name: componentName, type: 'composite', ports: children.__ports__ || {} }
? { name: componentName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ }
: { name: componentName, category: componentCategory }
);
console.log("DRAG START: Sending data ->", dragData);
@@ -2379,7 +2380,7 @@ Organization : OptiHK Limited
const cellName = children.__cellName__ || compositeName;
const tree = children.tree || {};
const handleDragStart = (event) => {
const dragData = JSON.stringify({ name: cellName, type: 'composite', ports: children.__ports__ || {} });
const dragData = JSON.stringify({ name: cellName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ });
event.dataTransfer.setData('application/reactflow', dragData);
event.dataTransfer.effectAllowed = 'move';
};
@@ -2584,7 +2585,7 @@ Organization : OptiHK Limited
);
} else {
return (
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId, __ports__: item.__ports__ || {} }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId, __ports__: item.__ports__ || {}, __boxSize__: item.__boxSize__ }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
);
}
})
@@ -2620,7 +2621,7 @@ Organization : OptiHK Limited
};
// Renders editable properties for selected nodes, ports, anchors, and routes.
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const [componentData, setComponentData] = useState(null);
const [loading, setLoading] = useState(false);
const [enlarged, setEnlarged] = useState(null);
@@ -2629,16 +2630,54 @@ Organization : OptiHK Limited
const [localX, setLocalX] = useState('');
const [localY, setLocalY] = useState('');
const [localRotation, setLocalRotation] = useState('');
const [editingTransformField, setEditingTransformField] = useState(null);
const MIXED_VALUE = '--';
const selectedPositionNodes = useMemo(
() => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position),
[selectedNodes, selectedNode]
);
const isMultiNodeSelection = selectedPositionNodes.length > 1;
const isPortRotationNode = useCallback((node) => (
node?.id === 'page-port' || node?.type === 'portNode' || node?.data?.elementType === 'port'
), []);
const getNodeRotationValue = useCallback((node) => (
isPortRotationNode(node) ? (node.data?.angle ?? 0) : (node.data?.rotation ?? 0)
), [isPortRotationNode]);
const getSharedNumericDisplay = useCallback((nodes, getValue) => {
if (!nodes.length) return '';
const firstValue = Number(getValue(nodes[0]));
if (!Number.isFinite(firstValue)) return MIXED_VALUE;
const sameValue = nodes.every(node => {
const nextValue = Number(getValue(node));
return Number.isFinite(nextValue) && Math.abs(nextValue - firstValue) < 0.0005;
});
return sameValue ? firstValue.toFixed(3) : MIXED_VALUE;
}, []);
const getSharedTextDisplay = useCallback((nodes, getValue) => {
if (!nodes.length) return '';
const firstValue = getValue(nodes[0]) || '';
return nodes.every(node => String(getValue(node) || '') === String(firstValue)) ? firstValue : MIXED_VALUE;
}, []);
const clearMixedInput = useCallback((event, setter) => {
if (event.currentTarget.value === MIXED_VALUE) {
setter('');
}
}, []);
const beginTransformInput = useCallback((event, field, setter) => {
setEditingTransformField(field);
clearMixedInput(event, setter);
}, [clearMixedInput]);
useEffect(() => {
const nodeId = selectedNode?.id;
if (!nodeId) {
if (!nodeId || isMultiNodeSelection) {
setComponentData(null);
setLoading(false);
return;
}
const compName = selectedNode?.data?.componentName;
if (selectedNode?.data?.elementType || isBasicComponent(compName)) {
const selectedIsComposite = selectedNode?.data?.type === 'composite' || compositeNames.includes(compName);
if (selectedNode?.data?.elementType || selectedIsComposite || isBasicComponent(compName)) {
setComponentData(null);
setLoading(false);
return;
@@ -2657,7 +2696,10 @@ Organization : OptiHK Limited
setLoading(true);
fetch(`/api/component/${encodeURIComponent(compName)}?project=${encodeURIComponent(projectName || '')}`)
.then(r => r.json())
.then(r => {
if (!r.ok) throw new Error('Component metadata not found');
return r.json();
})
.then(data => {
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
onUpdateNode(nodeId, {
@@ -2671,33 +2713,27 @@ Organization : OptiHK Limited
setLoading(false);
})
.catch(() => setLoading(false));
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName, projectName, onUpdateNode]);
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName, isMultiNodeSelection, compositeNames, projectName, onUpdateNode]);
useEffect(() => {
if (selectedNode) {
setLocalX(selectedNode.position.x.toFixed(3));
setLocalY(selectedNode.position.y.toFixed(3));
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
? (selectedNode.data?.angle ?? 0)
: (selectedNode.data?.rotation ?? 0);
setLocalRotation(rot.toFixed(3));
if (editingTransformField) return;
if (selectedPositionNodes.length > 0) {
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
return;
}
}, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.data?.angle, selectedNode?.id]);
const selectedPositionNodes = useMemo(
() => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position),
[selectedNodes, selectedNode]
);
setLocalX('');
setLocalY('');
setLocalRotation('');
}, [selectedPositionNodes, getSharedNumericDisplay, getNodeRotationValue, editingTransformField]);
const updatePosition = useCallback((id, axis, value) => {
const val = parseFloat(value);
if (isNaN(val)) return;
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
const baseNode = selectedPositionNodes.find(node => node.id === id) || selectedPositionNodes[0];
const delta = val - Number((baseNode.position && baseNode.position[axis]) || 0);
selectedPositionNodes.forEach(node => {
const currentValue = Number((node.position && node.position[axis]) || 0);
onUpdateNode(node.id, { position: { [axis]: currentValue + delta } });
onUpdateNode(node.id, { position: { [axis]: val } });
});
return;
}
@@ -2708,9 +2744,38 @@ Organization : OptiHK Limited
const val = parseFloat(value);
if (isNaN(val)) return;
const clamped = Math.min(180, Math.max(-180, val));
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
selectedPositionNodes.forEach(node => {
const dataField = isPortRotationNode(node) ? { angle: clamped } : { rotation: clamped };
onUpdateNode(node.id, { data: dataField });
});
return;
}
const dataField = isPortNode || id === 'page-port' ? { angle: clamped } : { rotation: clamped };
onUpdateNode(id, { data: dataField });
}, [onUpdateNode]);
}, [onUpdateNode, selectedPositionNodes, isPortRotationNode]);
const commitTransformInput = useCallback((event, field, setter) => {
const rawValue = event.currentTarget.value;
const val = parseFloat(rawValue);
if (!isNaN(val) && selectedNode) {
if (field === 'x') {
updatePosition(selectedNode.id, 'x', val);
} else if (field === 'y') {
updatePosition(selectedNode.id, 'y', val);
} else {
updateRotation(selectedNode.id, val, isPortRotationNode(selectedNode));
}
setter(val.toFixed(3));
} else if (field === 'x') {
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
} else if (field === 'y') {
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
} else {
setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
}
setEditingTransformField(null);
}, [selectedNode, selectedPositionNodes, updatePosition, updateRotation, isPortRotationNode, getSharedNumericDisplay, getNodeRotationValue]);
const toggleComponentTransform = useCallback((key) => {
if (!selectedNode) return;
@@ -2736,12 +2801,18 @@ Organization : OptiHK Limited
const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null;
const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {};
const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments);
const selectedIsPort = selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
const selectedIsAnchor = selectedNode?.data?.elementType === 'anchor';
const selectedNodeBoxSize = selectedNode?.data?.componentName && !selectedNode?.data?.elementType
const selectedIsPort = !isMultiNodeSelection && selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
const selectedIsAnchor = !isMultiNodeSelection && selectedNode?.data?.elementType === 'anchor';
const selectedNodeBoxSize = !isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType
? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE)
: null;
const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
const sharedComponentName = isMultiNodeSelection
? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentName || node.data?.elementType || node.type || '')
: '';
const sharedDisplayName = isMultiNodeSelection
? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentDisplayName || node.data?.label || node.id)
: '';
const selectedRouteEdges = selectedEdges.length > 0 ? selectedEdges : (selectedEdge ? [selectedEdge] : []);
if (selectedRouteEdges.length > 0) {
@@ -2908,19 +2979,12 @@ Organization : OptiHK Limited
<label>
<span>X</span>
<input
type="number"
type="text"
step="1"
value={localX}
onChange={(e) => setLocalX(e.target.value)}
onBlur={() => {
const val = parseFloat(localX);
if (!isNaN(val) && selectedNode) {
updatePosition(selectedNode.id, 'x', val);
setLocalX(val.toFixed(3));
} else if (selectedNode) {
setLocalX(selectedNode.position.x.toFixed(3));
}
}}
onFocus={(event) => beginTransformInput(event, 'x', setLocalX)}
onBlur={(event) => commitTransformInput(event, 'x', setLocalX)}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
}}
@@ -2929,19 +2993,12 @@ Organization : OptiHK Limited
<label>
<span>Y</span>
<input
type="number"
type="text"
step="1"
value={localY}
onChange={(e) => setLocalY(e.target.value)}
onBlur={() => {
const val = parseFloat(localY);
if (!isNaN(val) && selectedNode) {
updatePosition(selectedNode.id, 'y', val);
setLocalY(val.toFixed(3));
} else if (selectedNode) {
setLocalY(selectedNode.position.y.toFixed(3));
}
}}
onFocus={(event) => beginTransformInput(event, 'y', setLocalY)}
onBlur={(event) => commitTransformInput(event, 'y', setLocalY)}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
}}
@@ -2950,22 +3007,12 @@ Organization : OptiHK Limited
<label>
<span>Angle</span>
<input
type="number"
type="text"
step="1"
value={localRotation}
onChange={(e) => setLocalRotation(e.target.value)}
onBlur={() => {
const val = parseFloat(localRotation);
if (!isNaN(val) && selectedNode) {
updateRotation(selectedNode.id, val, selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
setLocalRotation(val.toFixed(3));
} else if (selectedNode) {
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
? (selectedNode.data?.angle ?? 0)
: (selectedNode.data?.rotation ?? 0);
setLocalRotation(rot.toFixed(3));
}
}}
onFocus={(event) => beginTransformInput(event, 'rotation', setLocalRotation)}
onBlur={(event) => commitTransformInput(event, 'rotation', setLocalRotation)}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
}}
@@ -2977,7 +3024,7 @@ Organization : OptiHK Limited
{selectedPositionNodes.length} selected
</div>
)}
{selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
{!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
<button
type="button"
@@ -3002,6 +3049,19 @@ Organization : OptiHK Limited
</div>
</div>
{isMultiNodeSelection && (
<div className="right-block" style={{ flexShrink: 0 }}>
<div className="right-block-header">Component Information</div>
<div className="right-block-body">
<div style={{ display: 'grid', gap: 8, color: 'var(--text-muted)' }}>
<div><strong style={{ color: 'var(--text-main)' }}>Selection:</strong> {selectedPositionNodes.length} components</div>
<div><strong style={{ color: 'var(--text-main)' }}>Name:</strong> {sharedDisplayName || MIXED_VALUE}</div>
<div><strong style={{ color: 'var(--text-main)' }}>Component:</strong> {sharedComponentName || MIXED_VALUE}</div>
</div>
</div>
</div>
)}
{selectedIsPort && (
<div className="right-block" style={{ flexShrink: 0 }}>
<div className="right-block-header">Port</div>
@@ -3095,7 +3155,7 @@ Organization : OptiHK Limited
</div>
)}
{selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
{!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
<div className="right-block" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div className="right-block-header">Parameters</div>
<div className="right-block-body" style={{ flex: 1, overflowY: 'auto' }}>
@@ -3508,6 +3568,8 @@ Organization : OptiHK Limited
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
const compositePageNames = useMemo(() => pages.filter(page => page.type === 'composite').map(page => page.name), [pages]);
const compositePageNameSet = useMemo(() => new Set(compositePageNames), [compositePageNames]);
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]);
@@ -3816,7 +3878,7 @@ Organization : OptiHK Limited
// Fetch metadata for a component before creating a loaded or dropped node.
const loadComponentMetadata = useCallback(async (componentName) => {
if (!componentName || isForgeComponent(componentName)) return null;
if (!componentName || isForgeComponent(componentName) || compositePageNameSet.has(componentName)) return null;
if (componentDataCacheRef.current.has(componentName)) {
return componentDataCacheRef.current.get(componentName);
}
@@ -3825,7 +3887,7 @@ Organization : OptiHK Limited
const data = await response.json();
componentDataCacheRef.current.set(componentName, data);
return data;
}, [currentProjectName]);
}, [currentProjectName, compositePageNameSet]);
// Send an auditable user action to the backend log endpoint.
const recordUserAction = useCallback((action, payload = {}) => {
@@ -4648,7 +4710,7 @@ Organization : OptiHK Limited
return category;
};
const pageFromYaml = (cellName, content, manifest) => {
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;
@@ -4676,6 +4738,7 @@ Organization : OptiHK Limited
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const instIsComposite = knownCompositeNames.has(compName);
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
@@ -4688,13 +4751,14 @@ Organization : OptiHK Limited
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
},
data: {
label: displayCompName,
componentName: displayCompName,
category: instIsForge ? '' : findCategory(displayCompName),
label: instIsComposite ? instName : displayCompName,
componentName: instIsComposite ? compName : displayCompName,
category: instIsComposite || instIsForge ? '' : findCategory(displayCompName),
rotation: parseFloat(inst.rotation) || 0,
flip: toBooleanFlag(inst.flip ?? inst.mirror),
flop: toBooleanFlag(inst.flop),
componentDisplayName: instName,
type: instIsComposite ? 'composite' : undefined,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
@@ -4763,7 +4827,18 @@ Organization : OptiHK Limited
const technology = data.technology || '';
setProjectTechnology(technology);
const manifest = await loadTechnologyManifest(technology);
const cellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content, manifest));
const knownCompositeNames = new Set((data.cells || []).map(cell => cell.name).filter(name => name !== currentProjectName));
const parsedCellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content, manifest, knownCompositeNames));
const compositeBoxSizes = new Map(parsedCellPages
.filter(page => page.type === 'composite')
.map(page => [page.name, calculateCompositeBoxSize(page)]));
const cellPages = parsedCellPages.map(page => ({
...page,
nodes: page.nodes.map(node => {
const boxSize = compositeBoxSizes.get(node.data?.componentName);
return boxSize ? { ...node, data: { ...node.data, boxSize } } : node;
})
}));
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
const resolvedProjectPage = loadedProjectPage || projectPage;
@@ -4810,23 +4885,26 @@ Organization : OptiHK Limited
useEffect(() => {
const compositePages = new Map(pages.filter(page => page.type === 'composite').map(page => [page.name, page]));
const portUpdates = new Map();
const compositeUpdates = new Map();
pages.forEach(page => {
page.nodes.forEach(node => {
const compPage = compositePages.get(node.data?.componentName);
if (!compPage) return;
const nextPorts = buildPageComponentPorts(compPage.port, compPage.nodes);
if (JSON.stringify(node.data?.ports || {}) !== JSON.stringify(nextPorts)) {
portUpdates.set(node.id, nextPorts);
const nextBoxSize = calculateCompositeBoxSize(compPage);
const portsChanged = JSON.stringify(node.data?.ports || {}) !== JSON.stringify(nextPorts);
const boxSizeChanged = JSON.stringify(node.data?.boxSize || {}) !== JSON.stringify(nextBoxSize);
if (portsChanged || boxSizeChanged) {
compositeUpdates.set(node.id, { ports: nextPorts, boxSize: nextBoxSize });
}
});
});
if (portUpdates.size === 0) return;
if (compositeUpdates.size === 0) return;
setPages(prev => prev.map(page => ({
...page,
nodes: page.nodes.map(node => (
portUpdates.has(node.id)
? { ...node, data: { ...node.data, ports: portUpdates.get(node.id) } }
compositeUpdates.has(node.id)
? { ...node, data: { ...node.data, ...compositeUpdates.get(node.id) } }
: node
))
})));
@@ -4837,7 +4915,7 @@ Organization : OptiHK Limited
pages.forEach(page => {
page.nodes.forEach(node => {
const componentName = node.data?.componentName;
if (node.data?.elementType || !componentName || isForgeComponent(componentName) || node.data?.type === 'composite') return;
if (node.data?.elementType || !componentName || isForgeComponent(componentName) || node.data?.type === 'composite' || compositePageNameSet.has(componentName)) return;
if (isBasicComponent(componentName)) {
if (node.data?.ports && node.data?.boxSize) return;
const metadata = getBasicComponentMetadata(componentName, node.data?.basicArguments);
@@ -5178,10 +5256,11 @@ Organization : OptiHK Limited
parsedData = { name: rawData, category: 'default' };
}
if (parsedData.type === 'standaloneComposite') {
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const position = clampPositionToCanvas(
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
activePage?.canvasSize || activeCanvasSize,
DEFAULT_COMPONENT_BOX_SIZE
compositeBoxSize
);
const newNode = {
id: Date.now().toString(),
@@ -5194,7 +5273,8 @@ Organization : OptiHK Limited
type: 'composite',
category: null,
rotation: 0,
ports: parsedData.ports || {}
ports: parsedData.ports || {},
boxSize: compositeBoxSize
}
};
setPages(prev => prev.map(p => {
@@ -5227,10 +5307,11 @@ Organization : OptiHK Limited
addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`);
return;
}
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const position = clampPositionToCanvas(
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
activePage?.canvasSize || activeCanvasSize,
DEFAULT_COMPONENT_BOX_SIZE
compositeBoxSize
);
const newNode = {
id: Date.now().toString(),
@@ -5243,7 +5324,8 @@ Organization : OptiHK Limited
type: 'composite',
category: null,
rotation: 0,
ports: parsedData.ports || {}
ports: parsedData.ports || {},
boxSize: compositeBoxSize
}
};
setPages(prev => prev.map(p => {
@@ -5651,7 +5733,8 @@ Organization : OptiHK Limited
__cellName__: componentName,
tree: compositeTrees[componentName] || {},
pageId: compPage.id,
__ports__: buildPageComponentPorts(compPage.port, compPage.nodes)
__ports__: buildPageComponentPorts(compPage.port, compPage.nodes),
__boxSize__: calculateCompositeBoxSize(compPage)
};
}
return {
@@ -5672,7 +5755,8 @@ Organization : OptiHK Limited
__cellName__: name,
tree: compositeTrees[name] || {},
pageId: compPage ? compPage.id : name,
__ports__: compPage ? buildPageComponentPorts(compPage.port, compPage.nodes) : {}
__ports__: compPage ? buildPageComponentPorts(compPage.port, compPage.nodes) : {},
__boxSize__: compPage ? calculateCompositeBoxSize(compPage) : DEFAULT_COMPONENT_BOX_SIZE
};
});
items.push({
@@ -5688,7 +5772,8 @@ Organization : OptiHK Limited
name: name,
tree: compositeTrees[name] || {},
pageId: compPage?.id || name,
__ports__: buildPageComponentPorts(compPage?.port, compPage?.nodes)
__ports__: buildPageComponentPorts(compPage?.port, compPage?.nodes),
__boxSize__: compPage ? calculateCompositeBoxSize(compPage) : DEFAULT_COMPONENT_BOX_SIZE
});
});
return items;
@@ -5705,7 +5790,8 @@ Organization : OptiHK Limited
__name__: page.name,
__category__: 'composite',
__cell__: true,
__ports__: buildPageComponentPorts(page.port, page.nodes)
__ports__: buildPageComponentPorts(page.port, page.nodes),
__boxSize__: calculateCompositeBoxSize(page)
};
});
const basicEntries = {
@@ -6159,6 +6245,7 @@ ${bundlesBlock}`;
selectedEdges={selectedEdges}
technologyManifest={technologyManifest}
projectName={currentProjectName}
compositeNames={compositePageNames}
width={rightWidth}
onRenameComponent={renameComponent}
onUpdateNode={handleUpdateNode}
+1014 -533
View File
File diff suppressed because it is too large Load Diff
+412 -179
View File
@@ -2,7 +2,7 @@
<!--
Description: Login page for authenticating users before entering the MXPIC EDA workspace.
Inside functions: applyTheme
Inside functions: applyTheme, setSubmitState
Developer : Qin Yue @ 2026
Organization : OptiHK Limited
-->
@@ -15,68 +15,82 @@ Organization : OptiHK Limited
<style>
:root {
--bg-main: #060b16;
--bg-panel: #0c1424;
--bg-card: #121b2d;
--bg-soft: #182237;
--bg-main: #06101d;
--bg-panel: #0c1728;
--bg-card: #111c2e;
--bg-soft: #17243a;
--text-main: #f6f8fb;
--text-muted: #91a0b5;
--accent: #6ee7ff;
--accent-strong: #7c3aed;
--accent-warm: #f97316;
--text-muted: #97a6ba;
--accent: #5dd8f3;
--accent-strong: #2563eb;
--accent-warm: #d97706;
--accent-green: #34d399;
--accent-red: #ef4444;
--border: #28364c;
--border-strong: #42516a;
--input-bg: #09111f;
--shadow: rgba(0, 0, 0, 0.42);
--border: #2a394f;
--border-strong: #496078;
--input-bg: #091320;
--shadow: rgba(0, 0, 0, 0.38);
--error-text: #fecaca;
--error-bg: rgba(239, 68, 68, 0.14);
--focus-ring: rgba(93, 216, 243, 0.28);
}
body.light-mode {
--bg-main: #edf3f8;
--bg-panel: #f8fbff;
--bg-main: #f4f7fb;
--bg-panel: #ffffff;
--bg-card: #ffffff;
--bg-soft: #eef5fb;
--text-main: #132032;
--text-muted: #64758a;
--accent: #2563eb;
--accent-strong: #0f9f7a;
--accent-warm: #38bdf8;
--bg-soft: #eef4fa;
--text-main: #142235;
--text-muted: #5f7085;
--accent: #1d4ed8;
--accent-strong: #0f766e;
--accent-warm: #b45309;
--accent-green: #15803d;
--accent-red: #dc2626;
--border: #d5e0eb;
--border-strong: #b8c7d8;
--input-bg: #f5f8fb;
--shadow: rgba(37, 99, 235, 0.13);
--error-text: #b91c1c;
--border: #d6e0eb;
--border-strong: #b6c4d4;
--input-bg: #f8fafc;
--shadow: rgba(37, 99, 235, 0.12);
--error-text: #991b1b;
--error-bg: rgba(220, 38, 38, 0.08);
--focus-ring: rgba(29, 78, 216, 0.2);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
color: var(--text-main);
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(0deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
radial-gradient(circle at 16% 12%, rgba(124, 58, 237, 0.25), transparent 28%),
radial-gradient(circle at 84% 82%, rgba(249, 115, 22, 0.16), transparent 26%),
linear-gradient(135deg, var(--bg-main), #0a1222 55%, #171923);
background-size: 42px 42px, 42px 42px, auto, auto, auto;
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
linear-gradient(0deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
linear-gradient(135deg, var(--bg-main), #091526 62%, #0d1724);
background-size: 48px 48px, 48px 48px, auto;
display: grid;
place-items: center;
padding: 28px;
box-sizing: border-box;
}
body.light-mode {
background:
linear-gradient(90deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px),
linear-gradient(0deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px),
radial-gradient(circle at 16% 12%, rgba(56, 189, 248, 0.18), transparent 28%),
radial-gradient(circle at 84% 82%, rgba(34, 197, 94, 0.13), transparent 24%),
linear-gradient(135deg, #f9fbfd, var(--bg-main));
linear-gradient(90deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
linear-gradient(0deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
linear-gradient(135deg, #f8fbff, var(--bg-main));
}
button,
input {
font: inherit;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
.theme-toggle {
@@ -84,110 +98,90 @@ Organization : OptiHK Limited
top: 20px;
right: 22px;
border: 1px solid var(--border);
background: rgba(18, 27, 45, 0.88);
background: rgba(12, 23, 40, 0.86);
color: var(--text-main);
border-radius: 8px;
min-width: 92px;
height: 36px;
padding: 0 14px;
padding: 0 12px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
box-shadow: 0 14px 34px var(--shadow);
backdrop-filter: blur(16px);
font-weight: 700;
box-shadow: 0 12px 28px var(--shadow);
backdrop-filter: blur(14px);
}
body.light-mode .theme-toggle {
background: rgba(255, 255, 255, 0.86);
}
.login-shell {
width: min(980px, 100%);
min-height: 560px;
display: grid;
grid-template-columns: minmax(0, 1fr) 400px;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: rgba(12, 20, 36, 0.84);
box-shadow: 0 32px 96px var(--shadow);
backdrop-filter: blur(18px);
}
body.light-mode .login-shell {
background: rgba(255, 255, 255, 0.9);
}
.login-shell {
width: min(1040px, 100%);
min-height: 590px;
display: grid;
grid-template-columns: minmax(0, 1fr) 410px;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: rgba(12, 23, 40, 0.88);
box-shadow: 0 30px 80px var(--shadow);
}
body.light-mode .login-shell {
background: rgba(255, 255, 255, 0.94);
}
.login-visual {
padding: 44px;
background:
linear-gradient(135deg, rgba(110, 231, 255, 0.1), transparent 35%),
linear-gradient(315deg, rgba(124, 58, 237, 0.16), transparent 42%),
var(--bg-panel);
padding: 38px;
background: var(--bg-panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 28px;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.login-visual::before {
content: "";
position: absolute;
inset: 92px 48px auto auto;
width: 190px;
height: 190px;
border: 1px solid rgba(110, 231, 255, 0.2);
border-radius: 50%;
box-shadow:
0 0 0 22px rgba(110, 231, 255, 0.025),
inset 0 0 0 28px rgba(249, 115, 22, 0.045);
}
.login-visual::after {
content: "";
position: absolute;
inset: auto 44px 130px auto;
width: 260px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: 0.55;
}
.login-visual > * {
position: relative;
z-index: 1;
.brand-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.brand-logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 1.25rem;
font-size: 1.02rem;
font-weight: 700;
letter-spacing: 0;
}
.brand-logo span {
.brand-logo .company-hk {
color: var(--accent);
}
.company-wordmark {
display: inline-flex;
gap: 0;
white-space: nowrap;
color: var(--text-main);
}
.brand-mark {
width: 36px;
height: 36px;
width: 34px;
height: 34px;
border-radius: 8px;
background:
linear-gradient(135deg, var(--accent), var(--accent-strong) 58%, var(--accent-warm));
background: linear-gradient(135deg, var(--accent), var(--accent-strong) 62%, var(--accent-warm));
position: relative;
box-shadow: 0 0 0 5px rgba(110, 231, 255, 0.09);
box-shadow: 0 0 0 5px rgba(93, 216, 243, 0.08);
}
.brand-mark::before,
.brand-mark::after {
content: "";
position: absolute;
background: white;
opacity: 0.9;
background: #ffffff;
opacity: 0.92;
}
.brand-mark::before {
@@ -204,71 +198,241 @@ Organization : OptiHK Limited
width: 2px;
}
.product-kicker {
color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.visual-copy h1 {
margin: 0 0 14px 0;
max-width: 430px;
font-size: clamp(2.1rem, 4vw, 3.25rem);
max-width: 520px;
font-size: clamp(2.25rem, 4.4vw, 3.7rem);
line-height: 1.02;
letter-spacing: 0;
}
.visual-copy p {
max-width: 430px;
max-width: 520px;
color: var(--text-muted);
line-height: 1.65;
margin: 0;
font-size: 1rem;
}
.status-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.status-pill {
.eda-preview {
border: 1px solid var(--border);
background: rgba(18, 27, 45, 0.72);
border-radius: 8px;
padding: 12px;
color: var(--text-muted);
font-size: 0.78rem;
overflow: hidden;
background: var(--bg-card);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
aspect-ratio: 16 / 9;
}
.status-pill strong {
.eda-preview img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-bar {
height: 36px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.72rem;
font-weight: 700;
}
.preview-status {
color: var(--accent-green);
}
.preview-workspace {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
min-height: 260px;
}
.preview-sidebar {
border-right: 1px solid var(--border);
background: var(--input-bg);
padding: 14px;
}
.tree-label {
color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.7rem;
font-weight: 700;
margin-bottom: 10px;
text-transform: uppercase;
}
.tree-row {
height: 28px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-main);
font-size: 0.92rem;
margin-bottom: 2px;
border-radius: 6px;
padding: 0 8px;
font-size: 0.82rem;
}
.tree-row.muted {
color: var(--text-muted);
padding-left: 22px;
}
.tree-dot {
width: 8px;
height: 8px;
border-radius: 99px;
background: var(--accent);
}
.cell-chip {
margin-top: 12px;
display: grid;
gap: 8px;
}
.cell-chip span {
display: block;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 9px;
color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.68rem;
background: var(--bg-soft);
}
.preview-canvas {
position: relative;
min-height: 260px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(0deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
var(--bg-card);
background-size: 28px 28px;
}
body.light-mode .preview-canvas {
background:
linear-gradient(90deg, rgba(29, 78, 216, 0.05) 1px, transparent 1px),
linear-gradient(0deg, rgba(29, 78, 216, 0.05) 1px, transparent 1px),
var(--bg-card);
background-size: 28px 28px;
}
.node {
position: absolute;
width: 94px;
height: 44px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: linear-gradient(180deg, var(--bg-soft), var(--bg-card));
color: var(--text-main);
display: grid;
place-items: center;
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.68rem;
font-weight: 700;
box-shadow: 0 14px 28px var(--shadow);
}
.node.a {
left: 42px;
top: 52px;
}
.node.b {
right: 46px;
top: 52px;
}
.node.c {
left: 50%;
top: 154px;
transform: translateX(-50%);
}
.route {
position: absolute;
height: 2px;
background: var(--accent);
box-shadow: 0 0 12px rgba(93, 216, 243, 0.45);
transform-origin: left center;
}
.route.one {
width: 124px;
left: 132px;
top: 74px;
}
.route.two {
width: 102px;
left: 122px;
top: 144px;
transform: rotate(34deg);
}
.route.three {
width: 102px;
right: 118px;
top: 144px;
transform: rotate(-34deg);
}
.login-card {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent),
linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent),
var(--bg-card);
padding: 44px;
padding: 42px;
display: flex;
flex-direction: column;
justify-content: center;
}
.mobile-product {
display: none;
margin-bottom: 22px;
}
.system-title {
color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.04em;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 10px;
}
.login-card h2 {
margin: 0 0 30px 0;
font-size: 1.48rem;
margin: 0 0 8px 0;
font-size: 1.5rem;
font-weight: 700;
}
.login-card .form-note {
color: var(--text-muted);
line-height: 1.5;
margin: 0 0 28px;
font-size: 0.94rem;
}
form {
display: flex;
flex-direction: column;
@@ -284,149 +448,218 @@ Organization : OptiHK Limited
label {
font-size: 0.84rem;
color: var(--text-muted);
font-weight: 600;
font-weight: 700;
}
.password-row {
position: relative;
}
input[type="text"],
input[type="password"] {
width: 100%;
min-height: 46px;
background-color: var(--input-bg);
border: 1px solid var(--border);
color: var(--text-main);
font-family: inherit;
font-size: 0.98rem;
padding: 13px 14px;
padding: 12px 14px;
border-radius: 8px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.password-row input {
padding-right: 70px;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: var(--accent);
background: var(--bg-panel);
box-shadow: 0 0 0 3px rgba(110, 231, 255, 0.15);
box-shadow: 0 0 0 3px var(--focus-ring);
}
button[type="submit"] {
.password-toggle {
position: absolute;
right: 7px;
top: 7px;
height: 32px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-soft);
color: var(--text-muted);
cursor: pointer;
padding: 0 10px;
font-size: 0.78rem;
font-weight: 700;
}
.submit-btn {
min-height: 46px;
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
color: #04101f;
font-family: inherit;
font-size: 0.98rem;
font-weight: 700;
padding: 13px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.2s ease;
margin-top: 8px;
box-shadow: 0 16px 34px rgba(37, 99, 235, 0.28);
transition: transform 0.1s ease, box-shadow 0.2s ease, opacity 0.2s ease;
margin-top: 4px;
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24);
}
button[type="submit"]:hover {
.submit-btn:hover:not(:disabled) {
transform: translateY(-1px);
}
button[type="submit"]:active {
.submit-btn:active:not(:disabled) {
transform: scale(0.99);
}
.submit-btn:disabled {
cursor: wait;
opacity: 0.72;
}
.error-message {
background-color: var(--error-bg);
color: var(--error-text);
border: 1px solid rgba(239, 68, 68, 0.38);
padding: 12px;
border: 1px solid rgba(239, 68, 68, 0.42);
padding: 11px 12px;
border-radius: 8px;
font-size: 0.85rem;
text-align: center;
margin-top: 20px;
font-size: 0.86rem;
line-height: 1.45;
margin-bottom: 2px;
}
@media (max-width: 800px) {
@media (max-width: 860px) {
body {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
padding: 18px;
}
.theme-toggle {
position: static;
align-self: flex-end;
margin-bottom: 14px;
}
.login-shell {
grid-template-columns: 1fr;
min-height: auto;
}
.login-visual {
display: none;
}
.theme-toggle {
position: static;
justify-self: end;
margin-bottom: 16px;
.mobile-product {
display: block;
}
body {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
.login-card {
padding: 30px 22px;
}
}
</style>
</head>
<body>
<button class="theme-toggle" id="theme-toggle" type="button">Bright Mode</button>
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch color theme">Light mode</button>
<main class="login-shell">
<section class="login-visual">
<div class="brand-logo">
<div class="brand-mark"></div>
opti<span>hk</span>
<section class="login-visual" aria-label="mxPIC EDA workspace preview">
<div class="brand-row">
<div class="brand-logo">
<div class="brand-mark" aria-hidden="true"></div>
MxPic - by <span class="company-wordmark">Opti<span class="company-hk">HK</span></span>
</div>
<div class="product-kicker">EDA platform</div>
</div>
<div class="visual-copy">
<h1>Photonic EDA workspace for reusable PIC cells.</h1>
<p>Build hierarchical canvases from PDK components, inspect ports and parameters, and prepare structured layout definitions for mxPIC generation.</p>
<h1>MxPIC</h1>
<p>A end-to-end EDA solution for photonic integrated</p>
</div>
<div class="status-strip">
<div class="status-pill"><strong>PDK</strong>Silterra ready</div>
<div class="status-pill"><strong>Cells</strong>Hierarchical</div>
<div class="status-pill"><strong>Flow</strong>Canvas to GDS</div>
<div class="eda-preview">
<img src="/frontend/assets/pic-mzm-directional-couplers.png" alt="2D render of a photonic integrated circuit with MZM arms and directional couplers">
</div>
</section>
<section class="login-card">
<div class="system-title">mxPIC Core Access</div>
<h2>Sign in to your EDA workspace</h2>
<div class="mobile-product">
<div class="brand-logo">
<div class="brand-mark" aria-hidden="true"></div>
MxPic - by <span class="company-wordmark">Opti<span class="company-hk">HK</span></span>
</div>
</div>
<form action="/login" method="POST">
<div class="system-title">Access</div>
<h2>Sign in</h2>
{% if error %}
<div class="error-message" role="alert">
{{ error }}
</div>
{% endif %}
<form action="/login" method="POST" id="login-form">
<div class="input-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
<input type="text" id="username" name="username" required autocomplete="username" autofocus>
</div>
<div class="input-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
<div class="password-row">
<input type="password" id="password" name="password" required autocomplete="current-password">
<button class="password-toggle" id="password-toggle" type="button" aria-label="Show password">Show</button>
</div>
</div>
<button type="submit">Log In</button>
<button class="submit-btn" id="submit-btn" type="submit">Log in</button>
</form>
{% if error %}
<div class="error-message">
{{ error }}
</div>
{% endif %}
</section>
</main>
<script>
const themeToggle = document.getElementById('theme-toggle');
const passwordInput = document.getElementById('password');
const passwordToggle = document.getElementById('password-toggle');
const loginForm = document.getElementById('login-form');
const submitButton = document.getElementById('submit-btn');
// Apply the selected login-page theme class and persist it for the next visit.
function applyTheme(mode) {
document.body.classList.toggle('light-mode', mode === 'light');
themeToggle.textContent = mode === 'light' ? 'Dark Mode' : 'Bright Mode';
const isLight = mode === 'light';
document.body.classList.toggle('light-mode', isLight);
themeToggle.textContent = isLight ? 'Dark mode' : 'Light mode';
localStorage.setItem('mxpic-theme', mode);
}
function setSubmitState() {
submitButton.disabled = true;
submitButton.textContent = 'Signing in...';
}
applyTheme(localStorage.getItem('mxpic-theme') || 'dark');
themeToggle.addEventListener('click', () => {
applyTheme(document.body.classList.contains('light-mode') ? 'dark' : 'light');
});
passwordToggle.addEventListener('click', () => {
const showPassword = passwordInput.type === 'password';
passwordInput.type = showPassword ? 'text' : 'password';
passwordToggle.textContent = showPassword ? 'Hide' : 'Show';
passwordToggle.setAttribute('aria-label', showPassword ? 'Hide password' : 'Show password');
});
loginForm.addEventListener('submit', setSubmitState);
</script>
</body>
</html>
+34
View File
@@ -119,6 +119,40 @@ assert.deepStrictEqual(
{ width: 118.8, height: 55.76 },
'default symbols should still scale proportionally inside normal component boxes'
);
assert.deepStrictEqual(
helpers.calculateCompositeBoxSize({
nodes: [
{
type: 'portNode',
position: { x: 10, y: 5 },
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10, width: 0.5 }
},
{
type: 'rotatableNode',
position: { x: 50, y: 20 },
data: { componentName: 'MMI_1', boxSize: { width: 80, height: 30 } }
},
{
type: 'rotatableNode',
position: { x: 160, y: 70 },
data: { componentName: 'MMI_2', boxSize: { width: 20, height: 10 } }
}
]
}),
{ width: 170, height: 75 },
'composite canvas symbols should use the bounds of exported ports and internal instance boxes'
);
assert.deepStrictEqual(
helpers.calculateCompositeBoxSize({
nodes: [{
type: 'portNode',
position: { x: 10, y: 5 },
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10 }
}]
}),
helpers.DEFAULT_COMPONENT_BOX_SIZE,
'single-port empty canvases should keep the default component footprint'
);
const rotatedHandles = helpers.buildPortHandles({
left_port: { x: -50, y: 0, a: 180 },
+12 -2
View File
@@ -379,9 +379,19 @@ assert(
);
assert(
canvasHtml.includes('selectedPositionNodes.length > 1') &&
canvasHtml.includes('const delta = val - Number') &&
canvasHtml.includes("const MIXED_VALUE = '--'") &&
canvasHtml.includes('getSharedNumericDisplay') &&
canvasHtml.includes('clearMixedInput') &&
canvasHtml.includes('event.currentTarget.value === MIXED_VALUE') &&
canvasHtml.includes('editingTransformField') &&
canvasHtml.includes('if (editingTransformField) return;') &&
canvasHtml.includes('commitTransformInput') &&
canvasHtml.includes('event.currentTarget.value') &&
canvasHtml.includes('updatePosition(selectedNode.id, \'x\', val)') &&
canvasHtml.includes('selectedPositionNodes.forEach(node => {') &&
canvasHtml.includes('position: { [axis]: val }') &&
canvasHtml.includes('selectedNodes={selectedNodes}'),
'multi-selected components should move together when editing X or Y in the inspector'
'multi-selected components should show mixed values as -- and set all selected X/Y values absolutely from the inspector'
);
assert(
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),