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 import yaml
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str: 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.""" """Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
layout = yaml.safe_load(yaml_content) or {} 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 kind: cell
coordinate_system: gds_y_up coordinate_system: gds_y_up
canvas_size: canvas_size:
width: 1000 width: 5000
height: 1000 height: 500
project: mxpic_project_1 project: mxpic_project_1
name: canvas_1 name: canvas_1
type: composite type: composite
@@ -16,35 +16,29 @@ version: "1.0.0"
ports: ports:
- name: port - name: port
layer: WG_CORE layer: WG_CORE
x: 200.0 x: 103.5
y: -370.0 y: -127.3
angle: 180.0
width: 0.5
- name: port_2
layer: WG_CORE
x: 200.0
y: -370.0
angle: 180.0 angle: 180.0
width: 0.5 width: 0.5
- name: port_1 - name: port_1
layer: WG_CORE layer: WG_CORE
x: 200.0 x: 108.7
y: -420.0 y: -252.6
angle: 180.0 angle: 180.0
width: 0.5 width: 0.5
- name: port_3 - name: port_2
layer: WG_CORE layer: WG_CORE
x: 691.9 x: 497.4
y: -267.5 y: -131.6
angle: 0.0 angle: 0.0
width: 0.5 width: 0.5
# 2. Instances (The sub-components dropped onto this canvas) # 2. Instances (The sub-components dropped onto this canvas)
instances: instances:
MMI_7: MMI_1:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 310.0 x: 177.9
y: -370.0 y: -252.1
rotation: 0.0 rotation: 0.0
flip: 0 flip: 0
flop: 0 flop: 0
@@ -52,54 +46,10 @@ instances:
settings: settings:
length: length:
MMI_8: MMI_2:
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
x: 560.0 x: 356.7
y: -130.0 y: -142.9
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
rotation: 0.0 rotation: 0.0
flip: 0 flip: 0
flop: 0 flop: 0
@@ -110,28 +60,8 @@ instances:
elements: elements:
port: port:
type: port type: port
x: 200.0 x: 103.5
y: -370.0 y: -127.3
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
angle: 0.0 angle: 0.0
port_number: 1 port_number: 1
pitch: 10 pitch: 10
@@ -140,18 +70,18 @@ elements:
description: "" description: ""
port_1: port_1:
type: port type: port
x: 200.0 x: 108.7
y: -420.0 y: -252.6
angle: 0.0 angle: 0.0
port_number: 1 port_number: 1
pitch: 10 pitch: 10
layer: WG_CORE layer: WG_CORE
width: 0.5 width: 0.5
description: "" description: ""
port_3: port_2:
type: port type: port
x: 691.9 x: 497.4
y: -267.5 y: -131.6
angle: 180.0 angle: 180.0
port_number: 1 port_number: 1
pitch: 10 pitch: 10
@@ -164,78 +94,22 @@ bundles:
output_bus: output_bus:
routing_type: euler_bend routing_type: euler_bend
links: links:
- from: MMI_7:a1 - from: MMI_1:a1
to: port_2:port
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: MMI_12:a1
to: port_1:port to: port_1:port
xsection: strip xsection: strip
family: optical family: optical
width: 0.45 width: 0.45
radius: 10 radius: 10
routing_type: euler_bend routing_type: euler_bend
- from: MMI_7:b1 - from: MMI_1:b1
to: Anchor_1:a1 to: MMI_2:a1
xsection: strip xsection: strip
family: optical family: optical
width: 0.45 width: 0.45
radius: 10 radius: 10
routing_type: euler_bend routing_type: euler_bend
- from: MMI_7:b2 - from: MMI_2:b1
to: Anchor_1:a2 to: port_2:port
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
xsection: strip xsection: strip
family: optical family: optical
width: 0.45 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) # 2. Instances (The sub-components dropped onto this canvas)
instances: instances:
MZM_1: canvas_1:
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZI_SiN400_Si220_PIN_mod_1310_L1300_QY_202603 component: canvas_1
x: 1740.0 x: 476.9
y: -2350.0 y: -1056.4
rotation: 0.0 rotation: 0.0
flip: 0 flip: 0
flop: 0 flop: 0
@@ -34,10 +34,10 @@ instances:
settings: settings:
length: length:
canvas_1: canvas_1_1:
component: canvas_1 component: canvas_1
x: 903.5 x: 1139.8
y: -2681.6 y: -958.5
rotation: 0.0 rotation: 0.0
flip: 0 flip: 0
flop: 0 flop: 0
@@ -62,15 +62,8 @@ bundles:
output_bus: output_bus:
routing_type: euler_bend routing_type: euler_bend
links: links:
- from: canvas_1:port_1 - from: canvas_1_1:port_1
to: MZM_1:a1 to: canvas_1:port_2
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
- from: canvas_1:port_3
to: MZM_1:a2
xsection: strip xsection: strip
family: optical family: optical
width: 0.45 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. // Round ruler measurements for compact display.
const roundMeasureValue = (value) => Number(value.toFixed(3)); const roundMeasureValue = (value) => Number(value.toFixed(3));
@@ -1073,6 +1120,7 @@ ${linksYaml}`;
normalizeCanvasSize, normalizeCanvasSize,
clampPositionToCanvas, clampPositionToCanvas,
calculateLayoutBounds, calculateLayoutBounds,
calculateCompositeBoxSize,
createRulerMeasurement, createRulerMeasurement,
createComponentSymbolMetrics, createComponentSymbolMetrics,
transformPortInfo, transformPortInfo,
+173 -86
View File
@@ -1456,6 +1456,7 @@ Organization : OptiHK Limited
normalizeCanvasSize, normalizeCanvasSize,
clampPositionToCanvas, clampPositionToCanvas,
calculateLayoutBounds, calculateLayoutBounds,
calculateCompositeBoxSize,
buildPortHandles, buildPortHandles,
buildElementPorts, buildElementPorts,
buildElementBoxSize, buildElementBoxSize,
@@ -2245,7 +2246,7 @@ Organization : OptiHK Limited
} }
const dragData = JSON.stringify( const dragData = JSON.stringify(
isUserCell isUserCell
? { name: componentName, type: 'composite', ports: children.__ports__ || {} } ? { name: componentName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ }
: { name: componentName, category: componentCategory } : { name: componentName, category: componentCategory }
); );
console.log("DRAG START: Sending data ->", dragData); console.log("DRAG START: Sending data ->", dragData);
@@ -2379,7 +2380,7 @@ Organization : OptiHK Limited
const cellName = children.__cellName__ || compositeName; const cellName = children.__cellName__ || compositeName;
const tree = children.tree || {}; const tree = children.tree || {};
const handleDragStart = (event) => { 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.setData('application/reactflow', dragData);
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
}; };
@@ -2584,7 +2585,7 @@ Organization : OptiHK Limited
); );
} else { } else {
return ( 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. // 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 [componentData, setComponentData] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [enlarged, setEnlarged] = useState(null); const [enlarged, setEnlarged] = useState(null);
@@ -2629,16 +2630,54 @@ Organization : OptiHK Limited
const [localX, setLocalX] = useState(''); const [localX, setLocalX] = useState('');
const [localY, setLocalY] = useState(''); const [localY, setLocalY] = useState('');
const [localRotation, setLocalRotation] = 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(() => { useEffect(() => {
const nodeId = selectedNode?.id; const nodeId = selectedNode?.id;
if (!nodeId) { if (!nodeId || isMultiNodeSelection) {
setComponentData(null); setComponentData(null);
setLoading(false); setLoading(false);
return; return;
} }
const compName = selectedNode?.data?.componentName; 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); setComponentData(null);
setLoading(false); setLoading(false);
return; return;
@@ -2657,7 +2696,10 @@ Organization : OptiHK Limited
setLoading(true); setLoading(true);
fetch(`/api/component/${encodeURIComponent(compName)}?project=${encodeURIComponent(projectName || '')}`) 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 => { .then(data => {
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name }); setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
onUpdateNode(nodeId, { onUpdateNode(nodeId, {
@@ -2671,33 +2713,27 @@ Organization : OptiHK Limited
setLoading(false); setLoading(false);
}) })
.catch(() => 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(() => { useEffect(() => {
if (selectedNode) { if (editingTransformField) return;
setLocalX(selectedNode.position.x.toFixed(3)); if (selectedPositionNodes.length > 0) {
setLocalY(selectedNode.position.y.toFixed(3)); setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port' setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
? (selectedNode.data?.angle ?? 0) setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
: (selectedNode.data?.rotation ?? 0); return;
setLocalRotation(rot.toFixed(3));
} }
}, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.data?.angle, selectedNode?.id]); setLocalX('');
setLocalY('');
const selectedPositionNodes = useMemo( setLocalRotation('');
() => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position), }, [selectedPositionNodes, getSharedNumericDisplay, getNodeRotationValue, editingTransformField]);
[selectedNodes, selectedNode]
);
const updatePosition = useCallback((id, axis, value) => { const updatePosition = useCallback((id, axis, value) => {
const val = parseFloat(value); const val = parseFloat(value);
if (isNaN(val)) return; if (isNaN(val)) return;
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) { 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 => { selectedPositionNodes.forEach(node => {
const currentValue = Number((node.position && node.position[axis]) || 0); onUpdateNode(node.id, { position: { [axis]: val } });
onUpdateNode(node.id, { position: { [axis]: currentValue + delta } });
}); });
return; return;
} }
@@ -2708,9 +2744,38 @@ Organization : OptiHK Limited
const val = parseFloat(value); const val = parseFloat(value);
if (isNaN(val)) return; if (isNaN(val)) return;
const clamped = Math.min(180, Math.max(-180, val)); 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 }; const dataField = isPortNode || id === 'page-port' ? { angle: clamped } : { rotation: clamped };
onUpdateNode(id, { data: dataField }); 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) => { const toggleComponentTransform = useCallback((key) => {
if (!selectedNode) return; if (!selectedNode) return;
@@ -2736,12 +2801,18 @@ Organization : OptiHK Limited
const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null; const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null;
const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {}; const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {};
const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments); const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments);
const selectedIsPort = selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'); const selectedIsPort = !isMultiNodeSelection && selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
const selectedIsAnchor = selectedNode?.data?.elementType === 'anchor'; const selectedIsAnchor = !isMultiNodeSelection && selectedNode?.data?.elementType === 'anchor';
const selectedNodeBoxSize = selectedNode?.data?.componentName && !selectedNode?.data?.elementType const selectedNodeBoxSize = !isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType
? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE) ? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE)
: null; : null;
const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {}); 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] : []); const selectedRouteEdges = selectedEdges.length > 0 ? selectedEdges : (selectedEdge ? [selectedEdge] : []);
if (selectedRouteEdges.length > 0) { if (selectedRouteEdges.length > 0) {
@@ -2908,19 +2979,12 @@ Organization : OptiHK Limited
<label> <label>
<span>X</span> <span>X</span>
<input <input
type="number" type="text"
step="1" step="1"
value={localX} value={localX}
onChange={(e) => setLocalX(e.target.value)} onChange={(e) => setLocalX(e.target.value)}
onBlur={() => { onFocus={(event) => beginTransformInput(event, 'x', setLocalX)}
const val = parseFloat(localX); onBlur={(event) => commitTransformInput(event, 'x', setLocalX)}
if (!isNaN(val) && selectedNode) {
updatePosition(selectedNode.id, 'x', val);
setLocalX(val.toFixed(3));
} else if (selectedNode) {
setLocalX(selectedNode.position.x.toFixed(3));
}
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Enter') e.currentTarget.blur();
}} }}
@@ -2929,19 +2993,12 @@ Organization : OptiHK Limited
<label> <label>
<span>Y</span> <span>Y</span>
<input <input
type="number" type="text"
step="1" step="1"
value={localY} value={localY}
onChange={(e) => setLocalY(e.target.value)} onChange={(e) => setLocalY(e.target.value)}
onBlur={() => { onFocus={(event) => beginTransformInput(event, 'y', setLocalY)}
const val = parseFloat(localY); onBlur={(event) => commitTransformInput(event, 'y', setLocalY)}
if (!isNaN(val) && selectedNode) {
updatePosition(selectedNode.id, 'y', val);
setLocalY(val.toFixed(3));
} else if (selectedNode) {
setLocalY(selectedNode.position.y.toFixed(3));
}
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Enter') e.currentTarget.blur();
}} }}
@@ -2950,22 +3007,12 @@ Organization : OptiHK Limited
<label> <label>
<span>Angle</span> <span>Angle</span>
<input <input
type="number" type="text"
step="1" step="1"
value={localRotation} value={localRotation}
onChange={(e) => setLocalRotation(e.target.value)} onChange={(e) => setLocalRotation(e.target.value)}
onBlur={() => { onFocus={(event) => beginTransformInput(event, 'rotation', setLocalRotation)}
const val = parseFloat(localRotation); onBlur={(event) => commitTransformInput(event, 'rotation', setLocalRotation)}
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));
}
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Enter') e.currentTarget.blur();
}} }}
@@ -2977,7 +3024,7 @@ Organization : OptiHK Limited
{selectedPositionNodes.length} selected {selectedPositionNodes.length} selected
</div> </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 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
<button <button
type="button" type="button"
@@ -3002,6 +3049,19 @@ Organization : OptiHK Limited
</div> </div>
</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 && ( {selectedIsPort && (
<div className="right-block" style={{ flexShrink: 0 }}> <div className="right-block" style={{ flexShrink: 0 }}>
<div className="right-block-header">Port</div> <div className="right-block-header">Port</div>
@@ -3095,7 +3155,7 @@ Organization : OptiHK Limited
</div> </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" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div className="right-block-header">Parameters</div> <div className="right-block-header">Parameters</div>
<div className="right-block-body" style={{ flex: 1, overflowY: 'auto' }}> <div className="right-block-body" style={{ flex: 1, overflowY: 'auto' }}>
@@ -3508,6 +3568,8 @@ Organization : OptiHK Limited
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []); const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]); const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
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 currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : []; const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]); 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. // Fetch metadata for a component before creating a loaded or dropped node.
const loadComponentMetadata = useCallback(async (componentName) => { 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)) { if (componentDataCacheRef.current.has(componentName)) {
return componentDataCacheRef.current.get(componentName); return componentDataCacheRef.current.get(componentName);
} }
@@ -3825,7 +3887,7 @@ Organization : OptiHK Limited
const data = await response.json(); const data = await response.json();
componentDataCacheRef.current.set(componentName, data); componentDataCacheRef.current.set(componentName, data);
return data; return data;
}, [currentProjectName]); }, [currentProjectName, compositePageNameSet]);
// Send an auditable user action to the backend log endpoint. // Send an auditable user action to the backend log endpoint.
const recordUserAction = useCallback((action, payload = {}) => { const recordUserAction = useCallback((action, payload = {}) => {
@@ -4648,7 +4710,7 @@ Organization : OptiHK Limited
return category; return category;
}; };
const pageFromYaml = (cellName, content, manifest) => { const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
const doc = jsyaml.load(content) || {}; const doc = jsyaml.load(content) || {};
const usesGdsYUp = doc.coordinate_system === 'gds_y_up'; const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null; const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
@@ -4676,6 +4738,7 @@ Organization : OptiHK Limited
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName); const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName); const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName); const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const instIsComposite = knownCompositeNames.has(compName);
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null; const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName); const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; 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), y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
}, },
data: { data: {
label: displayCompName, label: instIsComposite ? instName : displayCompName,
componentName: displayCompName, componentName: instIsComposite ? compName : displayCompName,
category: instIsForge ? '' : findCategory(displayCompName), category: instIsComposite || instIsForge ? '' : findCategory(displayCompName),
rotation: parseFloat(inst.rotation) || 0, rotation: parseFloat(inst.rotation) || 0,
flip: toBooleanFlag(inst.flip ?? inst.mirror), flip: toBooleanFlag(inst.flip ?? inst.mirror),
flop: toBooleanFlag(inst.flop), flop: toBooleanFlag(inst.flop),
componentDisplayName: instName, componentDisplayName: instName,
type: instIsComposite ? 'composite' : undefined,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents, availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined), ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined, boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
@@ -4763,7 +4827,18 @@ Organization : OptiHK Limited
const technology = data.technology || ''; const technology = data.technology || '';
setProjectTechnology(technology); setProjectTechnology(technology);
const manifest = await loadTechnologyManifest(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 loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage); const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
const resolvedProjectPage = loadedProjectPage || projectPage; const resolvedProjectPage = loadedProjectPage || projectPage;
@@ -4810,23 +4885,26 @@ Organization : OptiHK Limited
useEffect(() => { useEffect(() => {
const compositePages = new Map(pages.filter(page => page.type === 'composite').map(page => [page.name, page])); 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 => { pages.forEach(page => {
page.nodes.forEach(node => { page.nodes.forEach(node => {
const compPage = compositePages.get(node.data?.componentName); const compPage = compositePages.get(node.data?.componentName);
if (!compPage) return; if (!compPage) return;
const nextPorts = buildPageComponentPorts(compPage.port, compPage.nodes); const nextPorts = buildPageComponentPorts(compPage.port, compPage.nodes);
if (JSON.stringify(node.data?.ports || {}) !== JSON.stringify(nextPorts)) { const nextBoxSize = calculateCompositeBoxSize(compPage);
portUpdates.set(node.id, nextPorts); 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 => ({ setPages(prev => prev.map(page => ({
...page, ...page,
nodes: page.nodes.map(node => ( nodes: page.nodes.map(node => (
portUpdates.has(node.id) compositeUpdates.has(node.id)
? { ...node, data: { ...node.data, ports: portUpdates.get(node.id) } } ? { ...node, data: { ...node.data, ...compositeUpdates.get(node.id) } }
: node : node
)) ))
}))); })));
@@ -4837,7 +4915,7 @@ Organization : OptiHK Limited
pages.forEach(page => { pages.forEach(page => {
page.nodes.forEach(node => { page.nodes.forEach(node => {
const componentName = node.data?.componentName; 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 (isBasicComponent(componentName)) {
if (node.data?.ports && node.data?.boxSize) return; if (node.data?.ports && node.data?.boxSize) return;
const metadata = getBasicComponentMetadata(componentName, node.data?.basicArguments); const metadata = getBasicComponentMetadata(componentName, node.data?.basicArguments);
@@ -5178,10 +5256,11 @@ Organization : OptiHK Limited
parsedData = { name: rawData, category: 'default' }; parsedData = { name: rawData, category: 'default' };
} }
if (parsedData.type === 'standaloneComposite') { if (parsedData.type === 'standaloneComposite') {
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const position = clampPositionToCanvas( const position = clampPositionToCanvas(
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }), reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
activePage?.canvasSize || activeCanvasSize, activePage?.canvasSize || activeCanvasSize,
DEFAULT_COMPONENT_BOX_SIZE compositeBoxSize
); );
const newNode = { const newNode = {
id: Date.now().toString(), id: Date.now().toString(),
@@ -5194,7 +5273,8 @@ Organization : OptiHK Limited
type: 'composite', type: 'composite',
category: null, category: null,
rotation: 0, rotation: 0,
ports: parsedData.ports || {} ports: parsedData.ports || {},
boxSize: compositeBoxSize
} }
}; };
setPages(prev => prev.map(p => { setPages(prev => prev.map(p => {
@@ -5227,10 +5307,11 @@ Organization : OptiHK Limited
addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`); addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`);
return; return;
} }
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const position = clampPositionToCanvas( const position = clampPositionToCanvas(
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }), reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
activePage?.canvasSize || activeCanvasSize, activePage?.canvasSize || activeCanvasSize,
DEFAULT_COMPONENT_BOX_SIZE compositeBoxSize
); );
const newNode = { const newNode = {
id: Date.now().toString(), id: Date.now().toString(),
@@ -5243,7 +5324,8 @@ Organization : OptiHK Limited
type: 'composite', type: 'composite',
category: null, category: null,
rotation: 0, rotation: 0,
ports: parsedData.ports || {} ports: parsedData.ports || {},
boxSize: compositeBoxSize
} }
}; };
setPages(prev => prev.map(p => { setPages(prev => prev.map(p => {
@@ -5651,7 +5733,8 @@ Organization : OptiHK Limited
__cellName__: componentName, __cellName__: componentName,
tree: compositeTrees[componentName] || {}, tree: compositeTrees[componentName] || {},
pageId: compPage.id, pageId: compPage.id,
__ports__: buildPageComponentPorts(compPage.port, compPage.nodes) __ports__: buildPageComponentPorts(compPage.port, compPage.nodes),
__boxSize__: calculateCompositeBoxSize(compPage)
}; };
} }
return { return {
@@ -5672,7 +5755,8 @@ Organization : OptiHK Limited
__cellName__: name, __cellName__: name,
tree: compositeTrees[name] || {}, tree: compositeTrees[name] || {},
pageId: compPage ? compPage.id : 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({ items.push({
@@ -5688,7 +5772,8 @@ Organization : OptiHK Limited
name: name, name: name,
tree: compositeTrees[name] || {}, tree: compositeTrees[name] || {},
pageId: compPage?.id || 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; return items;
@@ -5705,7 +5790,8 @@ Organization : OptiHK Limited
__name__: page.name, __name__: page.name,
__category__: 'composite', __category__: 'composite',
__cell__: true, __cell__: true,
__ports__: buildPageComponentPorts(page.port, page.nodes) __ports__: buildPageComponentPorts(page.port, page.nodes),
__boxSize__: calculateCompositeBoxSize(page)
}; };
}); });
const basicEntries = { const basicEntries = {
@@ -6159,6 +6245,7 @@ ${bundlesBlock}`;
selectedEdges={selectedEdges} selectedEdges={selectedEdges}
technologyManifest={technologyManifest} technologyManifest={technologyManifest}
projectName={currentProjectName} projectName={currentProjectName}
compositeNames={compositePageNames}
width={rightWidth} width={rightWidth}
onRenameComponent={renameComponent} onRenameComponent={renameComponent}
onUpdateNode={handleUpdateNode} onUpdateNode={handleUpdateNode}
+970 -489
View File
File diff suppressed because it is too large Load Diff
+410 -177
View File
@@ -2,7 +2,7 @@
<!-- <!--
Description: Login page for authenticating users before entering the MXPIC EDA workspace. Description: Login page for authenticating users before entering the MXPIC EDA workspace.
Inside functions: applyTheme Inside functions: applyTheme, setSubmitState
Developer : Qin Yue @ 2026 Developer : Qin Yue @ 2026
Organization : OptiHK Limited Organization : OptiHK Limited
--> -->
@@ -15,68 +15,82 @@ Organization : OptiHK Limited
<style> <style>
:root { :root {
--bg-main: #060b16; --bg-main: #06101d;
--bg-panel: #0c1424; --bg-panel: #0c1728;
--bg-card: #121b2d; --bg-card: #111c2e;
--bg-soft: #182237; --bg-soft: #17243a;
--text-main: #f6f8fb; --text-main: #f6f8fb;
--text-muted: #91a0b5; --text-muted: #97a6ba;
--accent: #6ee7ff; --accent: #5dd8f3;
--accent-strong: #7c3aed; --accent-strong: #2563eb;
--accent-warm: #f97316; --accent-warm: #d97706;
--accent-green: #34d399;
--accent-red: #ef4444; --accent-red: #ef4444;
--border: #28364c; --border: #2a394f;
--border-strong: #42516a; --border-strong: #496078;
--input-bg: #09111f; --input-bg: #091320;
--shadow: rgba(0, 0, 0, 0.42); --shadow: rgba(0, 0, 0, 0.38);
--error-text: #fecaca; --error-text: #fecaca;
--error-bg: rgba(239, 68, 68, 0.14); --error-bg: rgba(239, 68, 68, 0.14);
--focus-ring: rgba(93, 216, 243, 0.28);
} }
body.light-mode { body.light-mode {
--bg-main: #edf3f8; --bg-main: #f4f7fb;
--bg-panel: #f8fbff; --bg-panel: #ffffff;
--bg-card: #ffffff; --bg-card: #ffffff;
--bg-soft: #eef5fb; --bg-soft: #eef4fa;
--text-main: #132032; --text-main: #142235;
--text-muted: #64758a; --text-muted: #5f7085;
--accent: #2563eb; --accent: #1d4ed8;
--accent-strong: #0f9f7a; --accent-strong: #0f766e;
--accent-warm: #38bdf8; --accent-warm: #b45309;
--accent-green: #15803d;
--accent-red: #dc2626; --accent-red: #dc2626;
--border: #d5e0eb; --border: #d6e0eb;
--border-strong: #b8c7d8; --border-strong: #b6c4d4;
--input-bg: #f5f8fb; --input-bg: #f8fafc;
--shadow: rgba(37, 99, 235, 0.13); --shadow: rgba(37, 99, 235, 0.12);
--error-text: #b91c1c; --error-text: #991b1b;
--error-bg: rgba(220, 38, 38, 0.08); --error-bg: rgba(220, 38, 38, 0.08);
--focus-ring: rgba(29, 78, 216, 0.2);
}
* {
box-sizing: border-box;
} }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100dvh;
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif; font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
color: var(--text-main); color: var(--text-main);
background: background:
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
linear-gradient(0deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), linear-gradient(0deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
radial-gradient(circle at 16% 12%, rgba(124, 58, 237, 0.25), transparent 28%), linear-gradient(135deg, var(--bg-main), #091526 62%, #0d1724);
radial-gradient(circle at 84% 82%, rgba(249, 115, 22, 0.16), transparent 26%), background-size: 48px 48px, 48px 48px, auto;
linear-gradient(135deg, var(--bg-main), #0a1222 55%, #171923);
background-size: 42px 42px, 42px 42px, auto, auto, auto;
display: grid; display: grid;
place-items: center; place-items: center;
padding: 28px; padding: 28px;
box-sizing: border-box;
} }
body.light-mode { body.light-mode {
background: background:
linear-gradient(90deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px), linear-gradient(90deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
linear-gradient(0deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px), linear-gradient(0deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
radial-gradient(circle at 16% 12%, rgba(56, 189, 248, 0.18), transparent 28%), linear-gradient(135deg, #f8fbff, var(--bg-main));
radial-gradient(circle at 84% 82%, rgba(34, 197, 94, 0.13), transparent 24%), }
linear-gradient(135deg, #f9fbfd, var(--bg-main));
button,
input {
font: inherit;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
} }
.theme-toggle { .theme-toggle {
@@ -84,110 +98,90 @@ Organization : OptiHK Limited
top: 20px; top: 20px;
right: 22px; right: 22px;
border: 1px solid var(--border); border: 1px solid var(--border);
background: rgba(18, 27, 45, 0.88); background: rgba(12, 23, 40, 0.86);
color: var(--text-main); color: var(--text-main);
border-radius: 8px; border-radius: 8px;
min-width: 92px;
height: 36px; height: 36px;
padding: 0 14px; padding: 0 12px;
cursor: pointer; cursor: pointer;
font-family: inherit; font-weight: 700;
font-weight: 600; box-shadow: 0 12px 28px var(--shadow);
box-shadow: 0 14px 34px var(--shadow); backdrop-filter: blur(14px);
backdrop-filter: blur(16px);
} }
body.light-mode .theme-toggle { 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); 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 { .login-visual {
padding: 44px; padding: 38px;
background: background: var(--bg-panel);
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);
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 28px;
justify-content: space-between; justify-content: space-between;
position: relative;
overflow: hidden;
} }
.login-visual::before { .brand-row {
content: ""; display: flex;
position: absolute; align-items: center;
inset: 92px 48px auto auto; justify-content: space-between;
width: 190px; gap: 18px;
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-logo { .brand-logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-size: 1.25rem; font-size: 1.02rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0; letter-spacing: 0;
} }
.brand-logo span { .brand-logo .company-hk {
color: var(--accent); color: var(--accent);
} }
.company-wordmark {
display: inline-flex;
gap: 0;
white-space: nowrap;
color: var(--text-main);
}
.brand-mark { .brand-mark {
width: 36px; width: 34px;
height: 36px; height: 34px;
border-radius: 8px; border-radius: 8px;
background: background: linear-gradient(135deg, var(--accent), var(--accent-strong) 62%, var(--accent-warm));
linear-gradient(135deg, var(--accent), var(--accent-strong) 58%, var(--accent-warm));
position: relative; 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::before,
.brand-mark::after { .brand-mark::after {
content: ""; content: "";
position: absolute; position: absolute;
background: white; background: #ffffff;
opacity: 0.9; opacity: 0.92;
} }
.brand-mark::before { .brand-mark::before {
@@ -204,71 +198,241 @@ Organization : OptiHK Limited
width: 2px; 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 { .visual-copy h1 {
margin: 0 0 14px 0; margin: 0 0 14px 0;
max-width: 430px; max-width: 520px;
font-size: clamp(2.1rem, 4vw, 3.25rem); font-size: clamp(2.25rem, 4.4vw, 3.7rem);
line-height: 1.02; line-height: 1.02;
letter-spacing: 0; letter-spacing: 0;
} }
.visual-copy p { .visual-copy p {
max-width: 430px; max-width: 520px;
color: var(--text-muted); color: var(--text-muted);
line-height: 1.65; line-height: 1.65;
margin: 0; margin: 0;
font-size: 1rem; font-size: 1rem;
} }
.status-strip { .eda-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.status-pill {
border: 1px solid var(--border); border: 1px solid var(--border);
background: rgba(18, 27, 45, 0.72);
border-radius: 8px; border-radius: 8px;
padding: 12px; overflow: hidden;
color: var(--text-muted); background: var(--bg-card);
font-size: 0.78rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
aspect-ratio: 16 / 9;
} }
.status-pill strong { .eda-preview img {
display: block; 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); color: var(--text-main);
font-size: 0.92rem; border-radius: 6px;
margin-bottom: 2px; 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 { .login-card {
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent), linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent),
var(--bg-card); var(--bg-card);
padding: 44px; padding: 42px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.mobile-product {
display: none;
margin-bottom: 22px;
}
.system-title { .system-title {
color: var(--text-muted); color: var(--text-muted);
font-family: 'IBM Plex Mono', Consolas, monospace; font-family: 'IBM Plex Mono', Consolas, monospace;
font-size: 0.74rem; font-size: 0.74rem;
font-weight: 600; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
margin-bottom: 10px; margin-bottom: 10px;
} }
.login-card h2 { .login-card h2 {
margin: 0 0 30px 0; margin: 0 0 8px 0;
font-size: 1.48rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
} }
.login-card .form-note {
color: var(--text-muted);
line-height: 1.5;
margin: 0 0 28px;
font-size: 0.94rem;
}
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -284,149 +448,218 @@ Organization : OptiHK Limited
label { label {
font-size: 0.84rem; font-size: 0.84rem;
color: var(--text-muted); color: var(--text-muted);
font-weight: 600; font-weight: 700;
}
.password-row {
position: relative;
} }
input[type="text"], input[type="text"],
input[type="password"] { input[type="password"] {
width: 100%;
min-height: 46px;
background-color: var(--input-bg); background-color: var(--input-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text-main); color: var(--text-main);
font-family: inherit;
font-size: 0.98rem; font-size: 0.98rem;
padding: 13px 14px; padding: 12px 14px;
border-radius: 8px; border-radius: 8px;
outline: none; outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; 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="text"]:focus,
input[type="password"]:focus { input[type="password"]:focus {
border-color: var(--accent); border-color: var(--accent);
background: var(--bg-panel); 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)); background: linear-gradient(135deg, var(--accent), var(--accent-strong));
color: #04101f; color: #04101f;
font-family: inherit;
font-size: 0.98rem; font-size: 0.98rem;
font-weight: 700; font-weight: 700;
padding: 13px; padding: 13px;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.2s ease; transition: transform 0.1s ease, box-shadow 0.2s ease, opacity 0.2s ease;
margin-top: 8px; margin-top: 4px;
box-shadow: 0 16px 34px rgba(37, 99, 235, 0.28); box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24);
} }
button[type="submit"]:hover { .submit-btn:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
} }
button[type="submit"]:active { .submit-btn:active:not(:disabled) {
transform: scale(0.99); transform: scale(0.99);
} }
.submit-btn:disabled {
cursor: wait;
opacity: 0.72;
}
.error-message { .error-message {
background-color: var(--error-bg); background-color: var(--error-bg);
color: var(--error-text); color: var(--error-text);
border: 1px solid rgba(239, 68, 68, 0.38); border: 1px solid rgba(239, 68, 68, 0.42);
padding: 12px; padding: 11px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 0.85rem; font-size: 0.86rem;
text-align: center; line-height: 1.45;
margin-top: 20px; margin-bottom: 2px;
}
@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;
} }
@media (max-width: 800px) {
.login-shell { .login-shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;
min-height: auto;
} }
.login-visual { .login-visual {
display: none; display: none;
} }
.theme-toggle { .mobile-product {
position: static; display: block;
justify-self: end;
margin-bottom: 16px;
} }
body { .login-card {
display: flex; padding: 30px 22px;
flex-direction: column;
align-items: stretch;
justify-content: center;
} }
} }
</style> </style>
</head> </head>
<body> <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"> <main class="login-shell">
<section class="login-visual"> <section class="login-visual" aria-label="mxPIC EDA workspace preview">
<div class="brand-row">
<div class="brand-logo"> <div class="brand-logo">
<div class="brand-mark"></div> <div class="brand-mark" aria-hidden="true"></div>
opti<span>hk</span> MxPic - by <span class="company-wordmark">Opti<span class="company-hk">HK</span></span>
</div>
<div class="product-kicker">EDA platform</div>
</div> </div>
<div class="visual-copy"> <div class="visual-copy">
<h1>Photonic EDA workspace for reusable PIC cells.</h1> <h1>MxPIC</h1>
<p>Build hierarchical canvases from PDK components, inspect ports and parameters, and prepare structured layout definitions for mxPIC generation.</p> <p>A end-to-end EDA solution for photonic integrated</p>
</div> </div>
<div class="status-strip"> <div class="eda-preview">
<div class="status-pill"><strong>PDK</strong>Silterra ready</div> <img src="/frontend/assets/pic-mzm-directional-couplers.png" alt="2D render of a photonic integrated circuit with MZM arms and directional couplers">
<div class="status-pill"><strong>Cells</strong>Hierarchical</div>
<div class="status-pill"><strong>Flow</strong>Canvas to GDS</div>
</div> </div>
</section> </section>
<section class="login-card"> <section class="login-card">
<div class="system-title">mxPIC Core Access</div> <div class="mobile-product">
<h2>Sign in to your EDA workspace</h2> <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"> <div class="input-group">
<label for="username">Username</label> <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>
<div class="input-group"> <div class="input-group">
<label for="password">Password</label> <label for="password">Password</label>
<div class="password-row">
<input type="password" id="password" name="password" required autocomplete="current-password"> <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> </div>
<button type="submit">Log In</button> <button class="submit-btn" id="submit-btn" type="submit">Log in</button>
</form> </form>
{% if error %}
<div class="error-message">
{{ error }}
</div>
{% endif %}
</section> </section>
</main> </main>
<script> <script>
const themeToggle = document.getElementById('theme-toggle'); 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) { function applyTheme(mode) {
document.body.classList.toggle('light-mode', mode === 'light'); const isLight = mode === 'light';
themeToggle.textContent = mode === 'light' ? 'Dark Mode' : 'Bright Mode'; document.body.classList.toggle('light-mode', isLight);
themeToggle.textContent = isLight ? 'Dark mode' : 'Light mode';
localStorage.setItem('mxpic-theme', mode); localStorage.setItem('mxpic-theme', mode);
} }
function setSubmitState() {
submitButton.disabled = true;
submitButton.textContent = 'Signing in...';
}
applyTheme(localStorage.getItem('mxpic-theme') || 'dark'); applyTheme(localStorage.getItem('mxpic-theme') || 'dark');
themeToggle.addEventListener('click', () => { themeToggle.addEventListener('click', () => {
applyTheme(document.body.classList.contains('light-mode') ? 'dark' : 'light'); 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> </script>
</body> </body>
</html> </html>
+34
View File
@@ -119,6 +119,40 @@ assert.deepStrictEqual(
{ width: 118.8, height: 55.76 }, { width: 118.8, height: 55.76 },
'default symbols should still scale proportionally inside normal component boxes' '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({ const rotatedHandles = helpers.buildPortHandles({
left_port: { x: -50, y: 0, a: 180 }, left_port: { x: -50, y: 0, a: 180 },
+12 -2
View File
@@ -379,9 +379,19 @@ assert(
); );
assert( assert(
canvasHtml.includes('selectedPositionNodes.length > 1') && 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}'), 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( assert(
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'), canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),