This commit is contained in:
xsxx03-art
2026-06-16 20:31:34 +08:00
parent 5db13a7d69
commit 944bc26fb9
21 changed files with 356 additions and 87 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+10 -2
View File
@@ -913,10 +913,18 @@ def getLib():
@app.route('/api/component/<component_name>')
@login_required_json
def getComp(component_name):
"""Return component YAML data."""
data = readCompYaml(component_name, pdk_root_for_request_project())
"""Return component YAML data with injected category."""
comps_root = pdk_root_for_request_project()
data = readCompYaml(component_name, comps_root)
if data is None:
return jsonify({"error": "Component not found"}), 404
# Derive the category (parent folder name) for icon resolution on the canvas.
if isinstance(data, dict) and '__category__' not in data:
search_root = comps_root or current_pdk_root()
for root, dirs, files in os.walk(search_root):
if os.path.basename(root) == component_name:
data['__category__'] = os.path.basename(os.path.dirname(root))
break
return jsonify(data)
@app.route('/api/component/<component_name>/image')
+32 -11
View File
@@ -12,6 +12,13 @@
}
root.MxpicCanvasHelpers = helpers;
})(typeof window !== 'undefined' ? window : globalThis, function () {
// Global origin for YAML coordinate export/import so YAML values always match
// the RightPanel display coordinate system.
let __exportOriginX = null;
let __exportOriginY = null;
var setExportOrigin = (x, y) => { __exportOriginX = x; __exportOriginY = y; };
var getExportOrigin = () => ({ x: __exportOriginX, y: __exportOriginY });
// Label used by the canvas to represent generated mxpic_forge components.
const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge';
// Serialized component type used when saving mxpic_forge-generated components.
@@ -651,10 +658,20 @@
return JSON.stringify(String(value));
};
// Convert canvas Y coordinates into layout Y coordinates.
const canvasToLayoutY = (value) => -Number(value || 0);
// Convert layout Y coordinates back into canvas Y coordinates.
const layoutToCanvasY = (value) => -Number(value || 0);
// Convert canvas coordinates into layout/display coordinates.
// When origin is set, uses the RightPanel formula: displayY = originY - rfY
// Falls back to simple negation for backward compatibility.
const canvasToLayoutY = (value) => { const v = Number(value || 0); return __exportOriginY != null ? __exportOriginY - v : -v; };
const canvasToLayoutX = (value) => { const v = Number(value || 0); return __exportOriginX != null ? v - __exportOriginX : v; };
// Convert layout/display coordinates back into canvas coordinates.
// Uses self-inverse formula for Y: rfY = originY - displayY (same as above).
// Import: layout/display coords → canvas coords. Optional originY/originX allow
// per-file overrides (e.g. from a YAML canvas_origin field). When omitted the
// module-level origin is used; null origin falls back to simple negation / pass-through.
const layoutToCanvasY = (value, originY) => { const v = Number(value || 0); const oy = originY !== undefined ? originY : __exportOriginY; return oy != null ? oy - v : -v; };
const layoutToCanvasX = (value, originX) => { const v = Number(value || 0); const ox = originX !== undefined ? originX : __exportOriginX; return ox != null ? v + ox : v; };
// Serialize nested component settings into YAML blocks.
const buildSettingsYaml = (settings, indent) => {
@@ -677,8 +694,8 @@
: '\n settings:\n length:';
return ` ${instanceName}:
component: ${componentValue}
x: ${Number(position.x || 0).toFixed(1)}
component: "${componentValue}"
x: ${canvasToLayoutX(position.x).toFixed(1)}
y: ${canvasToLayoutY(position.y).toFixed(1)}
rotation: ${Number(rotation || 0).toFixed(1)}
flip: ${flip ? 1 : 0}
@@ -908,7 +925,7 @@
// Convert standalone port nodes into page-level layout pins.
const buildPageComponentPins = (port, nodes) => {
const portNodes = (nodes || []).filter(isPortElementNode);
const portNodes = (nodes || []).filter(n => n.id === 'page-port');
if (portNodes.length > 0) {
return portNodes.reduce((pins, node) => {
const data = node.data || {};
@@ -962,7 +979,7 @@
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
element: ${info.element}
pin: ${info.pin}
x: ${Number(info.x || 0).toFixed(1)}
x: ${canvasToLayoutX(info.x).toFixed(1)}
y: ${canvasToLayoutY(info.y).toFixed(1)}
angle: ${Number(info.a || 0).toFixed(1)}
width: ${Number(info.width || 0.5)}${description}`;
@@ -977,7 +994,7 @@
// Serialize built-in port and anchor nodes into layout element metadata.
const buildElementsYaml = (nodes) => {
const elementNodes = (nodes || []).filter(isElementNode);
const elementNodes = (nodes || []).filter(n => isElementNode(n) && n.id !== 'page-port');
if (elementNodes.length === 0) return 'elements: {}';
const lines = elementNodes.map(node => {
const data = node.data || {};
@@ -990,7 +1007,7 @@
.join('\n');
return ` ${name}:
type: ${data.elementType}
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
x: ${canvasToLayoutX((node.position && node.position.x) || 0).toFixed(1)}
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
angle: ${Number(angle || 0).toFixed(1)}
pin_number: ${portNumber}
@@ -1055,7 +1072,7 @@ ${pinLines}`;
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
const pointsYaml = points.length > 0
? `\n points:\n${points.map(point => ` - x: ${Number(point.x || 0).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
? `\n points:\n${points.map(point => ` - x: ${canvasToLayoutX(point.x).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
: '';
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
const linkYaml = isFreeRoute
@@ -1311,8 +1328,12 @@ bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
DEFAULT_FORGE_ARGUMENTS,
FALLBACK_TECHNOLOGY_MANIFEST,
FREE_WIRES_BUNDLE_GROUP,
setExportOrigin,
getExportOrigin,
canvasToLayoutY,
canvasToLayoutX,
layoutToCanvasY,
layoutToCanvasX,
createForgeArguments,
createRouteSettings,
updateRouteField,
+290 -73
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html>
<!-- CANVAS v22: per-page instance counter — reserveComponentDisplayNamesFromPages only scans active page, not whole project -->
<!--
Description: Main MXPIC EDA canvas UI with React Flow editing, project pages, routing, layout build controls, and inspector panels.
Inside functions: fetchIcon, RotatableNode, PortNode, AnchorNode, RightPanel, findComponentPath, loadProject, handleBasicConnection, buildElementNodesFromYaml.
@@ -1467,11 +1468,47 @@ Organization : OptiHK Limited
height: 100%;
box-sizing: border-box;
border: 4px solid rgba(69, 214, 200, 0.92);
background: rgba(69, 214, 200, 0.018);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14), 0 0 22px rgba(69, 214, 200, 0.18);
background: rgba(69, 214, 200, 0.06);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 0 28px rgba(69, 214, 200, 0.28);
pointer-events: none;
}
body.light-mode .canvas-boundary-node {
border: 4px solid rgba(8, 127, 115, 0.9);
background: rgba(8, 127, 115, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7), 0 0 32px rgba(8, 127, 115, 0.32);
}
.origin-marker-node {
width: 16px;
height: 16px;
transform: translate(-50%, -50%);
pointer-events: none;
position: relative;
}
.origin-marker-node::before,
.origin-marker-node::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
background: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
}
.origin-marker-node::before {
width: 100%;
height: 2px;
transform: translate(-50%, -50%) rotate(45deg);
}
.origin-marker-node::after {
width: 100%;
height: 2px;
transform: translate(-50%, -50%) rotate(-45deg);
}
.ruler-point-node {
width: 12px;
height: 12px;
@@ -1588,6 +1625,10 @@ Organization : OptiHK Limited
createRulerMeasurement,
createComponentSymbolMetrics,
FALLBACK_TECHNOLOGY_MANIFEST,
setExportOrigin,
getExportOrigin,
canvasToLayoutX,
layoutToCanvasX,
layoutToCanvasY
} = window.MxpicCanvasHelpers;
@@ -1607,6 +1648,7 @@ Organization : OptiHK Limited
// Loads and caches category icons so repeated library renders do not refetch the same image.
function fetchIcon(category) {
if (!iconPromiseCache[category]) {
let resolveFn;
const promise = new Promise((resolve) => {
resolveFn = resolve;
@@ -2110,6 +2152,11 @@ Organization : OptiHK Limited
<div className="canvas-boundary-node" title={`${data.size.width} x ${data.size.height} um`} />
));
// Red "X" marker at the canvas origin (0,0). Non-selectable, always visible.
const OriginMarkerNode = memo(({ data }) => (
<div className="origin-marker-node" title={`Canvas origin (${(data.originX ?? 0).toFixed(1)}, ${(data.originY ?? 0).toFixed(1)})`} />
));
// Draws invisible connection handles for ruler measurement endpoints.
const RulerPointNode = memo(({ data }) => {
const hiddenHandleStyle = {
@@ -2903,7 +2950,7 @@ Organization : OptiHK Limited
};
// Renders editable properties for selected nodes, ports, anchors, and routes.
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], bundleGroupOptions = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], bundleGroupOptions = [], technologyManifest, projectName, compositeNames = [], width, canvasOrigin, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const [componentData, setComponentData] = useState(null);
const [loading, setLoading] = useState(false);
const [enlarged, setEnlarged] = useState(null);
@@ -3001,8 +3048,8 @@ Organization : OptiHK Limited
useEffect(() => {
if (editingTransformField) return;
if (selectedPositionNodes.length > 0) {
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x - canvasOrigin.x));
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => canvasOrigin.y - node.position.y));
setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
return;
}
@@ -3014,14 +3061,16 @@ Organization : OptiHK Limited
const updatePosition = useCallback((id, axis, value) => {
const val = parseFloat(value);
if (isNaN(val)) return;
// Convert display coordinates back to ReactFlow coordinates.
const rfVal = axis === 'y' ? canvasOrigin.y - val : val + canvasOrigin.x;
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
selectedPositionNodes.forEach(node => {
onUpdateNode(node.id, { position: { [axis]: val } });
onUpdateNode(node.id, { position: { [axis]: rfVal } });
});
return;
}
onUpdateNode(id, { position: { [axis]: val } });
}, [onUpdateNode, selectedPositionNodes]);
onUpdateNode(id, { position: { [axis]: rfVal } });
}, [onUpdateNode, selectedPositionNodes, canvasOrigin]);
const updateRotation = useCallback((id, value, isPortNode = false) => {
const val = parseFloat(value);
@@ -3051,9 +3100,9 @@ Organization : OptiHK Limited
}
setter(val.toFixed(3));
} else if (field === 'x') {
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x - canvasOrigin.x));
} else if (field === 'y') {
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
setter(getSharedNumericDisplay(selectedPositionNodes, node => canvasOrigin.y - node.position.y));
} else {
setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
}
@@ -3920,6 +3969,9 @@ Organization : OptiHK Limited
const [gridSnap, setGridSnap] = useState(false);
const [canvasTextVisible, setCanvasTextVisible] = useState(true);
const [themeMode, setThemeMode] = useState(() => localStorage.getItem('mxpic-theme') || 'dark');
const [closedTabs, setClosedTabs] = useState(() => {
try { return JSON.parse(localStorage.getItem('mxpic-closed-tabs') || '[]'); } catch { return []; }
});
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), message: 'Editor ready.' }]);
const [buildLayoutBusy, setBuildLayoutBusy] = useState(false);
const [buildGdsBusy, setBuildGdsBusy] = useState(false);
@@ -3949,6 +4001,36 @@ Organization : OptiHK Limited
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]);
// Track viewport dimensions to compute dynamic zoom limits.
const [canvasViewportSize, setCanvasViewportSize] = React.useState({ width: 1200, height: 800 });
// Set origin from the active page's saved value, or default to (500, H-500).
useEffect(() => {
if (activePage?.origin) {
setCanvasOrigin({ x: activePage.origin.x, y: activePage.origin.y });
} else if (activePage) {
setCanvasOrigin({ x: 500, y: activeCanvasSize.height - 500 });
}
}, [activePage?.id, activePage?.origin?.x, activePage?.origin?.y, activeCanvasSize.height]);
// Observe viewport size changes to recalculate minZoom.
useEffect(() => {
const el = canvasViewportRef.current;
if (!el) return;
const update = () => setCanvasViewportSize({ width: el.clientWidth, height: el.clientHeight });
update();
if (typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
// Keep the global export origin in sync so YAML coordinates match RightPanel display.
useEffect(() => {
setExportOrigin(canvasOrigin.x, canvasOrigin.y);
}, [canvasOrigin.x, canvasOrigin.y]);
const selectedEdges = useMemo(() => currentEdges.filter(edge => edge.selected), [currentEdges]);
const selectedEdge = selectedEdges[0] || null;
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
@@ -3992,6 +4074,15 @@ Organization : OptiHK Limited
}
}, [linkXsectionChoices, currentLinkXsection]);
const canvasNodeExtent = useMemo(() => [[0, 0], [activeCanvasSize.width, activeCanvasSize.height]], [activeCanvasSize.width, activeCanvasSize.height]);
// Dynamic minZoom: viewport can show at most 120% of canvas size.
const canvasMinZoom = useMemo(() => {
const cw = activeCanvasSize.width;
const ch = activeCanvasSize.height;
const vw = canvasViewportSize.width;
const vh = canvasViewportSize.height;
if (!cw || !ch || !vw || !vh) return 0.05;
return Math.min(vw / cw, vh / ch) / 1.2;
}, [activeCanvasSize.width, activeCanvasSize.height, canvasViewportSize.width, canvasViewportSize.height]);
const rulerActiveEndPoint = rulerEndPoint || rulerPreviewPoint;
const rulerMeasurement = useMemo(
() => createRulerMeasurement(rulerStartPoint, rulerActiveEndPoint),
@@ -4002,7 +4093,7 @@ Organization : OptiHK Limited
mouseCanvasPoint
? {
x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)),
y: Number((mouseCanvasPoint.y - canvasOrigin.y).toFixed(3))
y: Number((canvasOrigin.y - mouseCanvasPoint.y).toFixed(3))
}
: null
), [mouseCanvasPoint, canvasOrigin]);
@@ -4149,8 +4240,18 @@ Organization : OptiHK Limited
deletable: false,
focusable: false,
style: { width: activeCanvasSize.width, height: activeCanvasSize.height, zIndex: -1, pointerEvents: 'none' }
}, {
id: '__origin-marker__',
type: 'originMarkerNode',
position: { x: canvasOrigin.x, y: canvasOrigin.y },
data: { originX: canvasOrigin.x, originY: canvasOrigin.y },
draggable: false,
selectable: false,
deletable: false,
focusable: false,
style: { zIndex: 10, pointerEvents: 'none' }
}, ...currentNodes, ...freeRouteEndpointNodes, ...rulerNodes];
}, [activePage, currentNodes, activeCanvasSize, freeRouteEndpointNodes, rulerNodes]);
}, [activePage, currentNodes, activeCanvasSize, canvasOrigin, freeRouteEndpointNodes, rulerNodes]);
// Resolves rotated anchor handle direction so connected canvas links exit the correct side.
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
@@ -4253,12 +4354,12 @@ Organization : OptiHK Limited
value === true || value === 1 || value === '1' || String(value).toLowerCase() === 'true'
), []);
// Normalize stored route points and convert layout Y coordinates when needed.
// Normalize stored route points and convert layout coordinates back to canvas coordinates.
const normalizeRoutePoints = useCallback((points, usesGdsYUp = false) => (
(Array.isArray(points) ? points : [])
.map(point => ({
x: Number(point && point.x),
y: usesGdsYUp ? layoutToCanvasY(point && point.y) : Number(point && point.y)
x: usesGdsYUp ? layoutToCanvasX(point && point.x) : Number(point && point.x || 0),
y: usesGdsYUp ? layoutToCanvasY(point && point.y) : Number(point && point.y || 0)
}))
.filter(point => Number.isFinite(point.x) && Number.isFinite(point.y))
), []);
@@ -4297,7 +4398,10 @@ Organization : OptiHK Limited
return componentDataCacheRef.current.get(componentName);
}
const response = await fetch(`/api/component/${encodeURIComponent(componentName)}?project=${encodeURIComponent(currentProjectName)}`);
if (!response.ok) return null;
if (!response.ok) {
componentDataCacheRef.current.set(componentName, null);
return null;
}
const data = await response.json();
componentDataCacheRef.current.set(componentName, data);
return data;
@@ -4359,7 +4463,7 @@ Organization : OptiHK Limited
// Apply React Flow node changes while preserving canvas-only helper nodes.
const onNodesChange = useCallback((changes) => {
if (!activePageId) return;
const relevantChanges = changes.filter(change => change.id !== '__canvas-boundary__');
const relevantChanges = changes.filter(change => change.id !== '__canvas-boundary__' && change.id !== '__origin-marker__');
if (relevantChanges.length === 0) return;
const removedNodeIds = new Set(relevantChanges.filter(change => change.type === 'remove').map(change => change.id));
if (removedNodeIds.size > 0 && activePage) {
@@ -4714,11 +4818,17 @@ Organization : OptiHK Limited
nodes.forEach(node => releaseComponentDisplayName(node?.data?.componentDisplayName));
}
// Rebuild the used-name index table from all currently loaded pages.
// Rebuild the used-name index table from the current active page only.
function reserveComponentDisplayNamesFromPages() {
pages.forEach(page => {
(page.nodes || []).forEach(node => reserveComponentDisplayName(node?.data?.componentDisplayName));
});
componentIndexesByPrefixRef.current = {};
if (activePage) {
(activePage.nodes || []).forEach(node => {
const name = node?.data?.componentDisplayName;
if (name) {
reserveComponentDisplayName(name);
}
});
}
}
// Convert a component category into the saved display-name prefix or abbreviation.
@@ -4858,7 +4968,7 @@ Organization : OptiHK Limited
id: nodeId,
type: 'portNode',
position: {
x,
x: usesGdsYUp ? layoutToCanvasX(x) : x,
y: usesGdsYUp ? layoutToCanvasY(y) : y,
},
data: {
@@ -4875,7 +4985,7 @@ Organization : OptiHK Limited
id: nodeId,
type: 'anchorNode',
position: {
x,
x: usesGdsYUp ? layoutToCanvasX(x) : x,
y: usesGdsYUp ? layoutToCanvasY(y) : y,
},
data: {
@@ -4910,6 +5020,9 @@ Organization : OptiHK Limited
const text = await file.text();
const doc = jsyaml.load(text);
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
const savedOrigin = getExportOrigin();
const docOrigin = doc.origin ? { x: Number(doc.origin.x || 0), y: Number(doc.origin.y || 0) } : null;
if (docOrigin) setExportOrigin(docOrigin.x, docOrigin.y);
if (!doc.instances && !doc.elements) {
alert('no instances or elements found');
return;
@@ -4927,13 +5040,14 @@ Organization : OptiHK Limited
const compPath = inst.component || '';
const compName = compPath.split('/').pop();
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const instIsPDK = compPath.includes('/');
const instIsBasic = !instIsPDK && (isBasicComponent(compPath) || isBasicComponent(compName));
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : compName;
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
let category = '';
if (!isProject && displayCompName && library && !instIsForge) {
if (displayCompName && library && !instIsForge) {
const walk = (obj) => {
if (obj?.__type__ === 'component' && obj.__name__ === displayCompName) {
category = obj.__category__ || '';
@@ -4945,6 +5059,9 @@ Organization : OptiHK Limited
return false;
};
walk(library);
if (!category && compPath.includes('/')) {
category = compPath.split('/').slice(-2, -1)[0];
}
}
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
@@ -4954,12 +5071,12 @@ Organization : OptiHK Limited
id: nodeId,
type: 'rotatableNode',
position: {
x: parseFloat(inst.x) || 0,
x: usesGdsYUp ? layoutToCanvasX(inst.x) : (parseFloat(inst.x) || 0),
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
},
data: {
label: isProject ? instName : displayCompName,
componentName: isProject ? instName : displayCompName,
componentName: displayCompName,
category: isProject ? '' : category,
rotation: parseFloat(inst.rotation) || 0,
flip: toBooleanFlag(inst.flip ?? inst.mirror),
@@ -4976,6 +5093,30 @@ Organization : OptiHK Limited
}
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
// Pre-fetch PDK component metadata so imported nodes render with correct boxSize/ports/icons.
const pdkNames = [...new Set(newNodes
.filter(n => n.data?.componentName && !n.data?.elementType
&& !isForgeComponent(n.data.componentName)
&& !isBasicComponent(n.data.componentName))
.map(n => n.data.componentName))];
if (pdkNames.length > 0) {
const metaResults = await Promise.all(
pdkNames.map(name => loadComponentMetadata(name).catch(() => null))
);
const metaMap = new Map(
pdkNames.filter((_, i) => metaResults[i]).map((name, i) => [name, metaResults[i]])
);
newNodes.forEach((node, idx) => {
const metadata = metaMap.get(node.data?.componentName);
if (!metadata) return;
const sz = normalizeBoxSize(metadata);
newNodes[idx] = {
...node,
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '', category: node.data.category || metadata.__category__ || '' }
};
});
}
forEachBundleLink(doc, (bundleName, bundle, link) => {
const route = createRouteSettings(technologyManifest, { ...bundle, ...link, bundle_group: bundleName });
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
@@ -5010,8 +5151,8 @@ Organization : OptiHK Limited
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
const importedPin = Array.isArray(doc.pins) && doc.pins[0]
? { x: Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
: { x: 50, y: 150, a: 0, width: 0.5 };
? { x: usesGdsYUp ? layoutToCanvasX(doc.pins[0].x) : Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
: { x: 600, y: 4400, a: 0, width: 0.5 };
const newPage = {
id: newPageId,
name: newPageName,
@@ -5031,8 +5172,12 @@ Organization : OptiHK Limited
],
edges: newEdges,
port: importedPin,
origin: docOrigin || undefined,
};
// Restore the previous origin now that all coordinate conversions are done.
if (docOrigin) setExportOrigin(savedOrigin.x, savedOrigin.y);
setPages(prev => [...prev, newPage]);
setActivePageId(newPageId);
@@ -5081,7 +5226,7 @@ Organization : OptiHK Limited
input.addEventListener('change', handleFile);
return () => input.removeEventListener('change', handleFile);
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle, loadComponentMetadata]);
useEffect(() => {
setProjectCompositeMap(prev => {
@@ -5106,10 +5251,20 @@ Organization : OptiHK Limited
id: `project-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
name: currentProjectName,
type: 'project',
nodes: [],
nodes: [
{
id: 'page-port',
type: 'portNode',
position: { x: 600, y: 4400 },
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
draggable: true,
selectable: true,
deletable: false,
}
],
edges: [],
canvasSize: DEFAULT_CANVAS_SIZE,
port: { x: 0, y: 0, a: 0 }
port: { x: 600, y: 4400, a: 0 }
});
const findCategory = (compName) => {
@@ -5131,13 +5286,18 @@ Organization : OptiHK Limited
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
const doc = jsyaml.load(content) || {};
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
// Temporarily use the YAML's origin for coordinate conversion (if present)
// so that instances/ports/routes land at the correct canvas positions.
const savedOrigin = getExportOrigin();
const docOrigin = doc.origin ? { x: Number(doc.origin.x || 0), y: Number(doc.origin.y || 0) } : null;
if (docOrigin) setExportOrigin(docOrigin.x, docOrigin.y);
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
const pagePort = firstPin
? { x: Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
: { x: 50, y: 150, a: 0, width: 0.5 };
const nodeNameMap = {};
const nodes = [
{
? { x: usesGdsYUp ? layoutToCanvasX(firstPin.x) : Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
: null;
const nodes = [];
if (pagePort) {
nodes.push({
id: 'page-port',
type: 'portNode',
position: { x: pagePort.x, y: pagePort.y },
@@ -5145,17 +5305,21 @@ Organization : OptiHK Limited
draggable: true,
selectable: true,
deletable: false,
}
];
});
}
const nodeNameMap = {};
const edges = [];
nodeNameMap.port = 'page-port';
if (pagePort) {
nodeNameMap.port = 'page-port';
}
Object.entries(doc.instances || {}).forEach(([instName, inst]) => {
const compPath = inst.component || '';
const compName = compPath.split('/').pop();
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const instIsPDK = compPath.includes('/');
const instIsBasic = !instIsPDK && (isBasicComponent(compPath) || isBasicComponent(compName));
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : compName;
const instIsComposite = knownCompositeNames.has(compName);
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
@@ -5165,13 +5329,13 @@ Organization : OptiHK Limited
id: nodeId,
type: 'rotatableNode',
position: {
x: parseFloat(inst.x) || 0,
x: usesGdsYUp ? layoutToCanvasX(inst.x) : (parseFloat(inst.x) || 0),
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
},
data: {
label: instIsComposite ? instName : displayCompName,
componentName: instIsComposite ? compName : displayCompName,
category: instIsComposite || instIsForge ? '' : findCategory(displayCompName),
category: instIsComposite || instIsForge ? '' : (findCategory(displayCompName) || (compPath.includes('/') ? compPath.split('/').slice(-2, -1)[0] : '')),
rotation: parseFloat(inst.rotation) || 0,
flip: toBooleanFlag(inst.flip ?? inst.mirror),
flop: toBooleanFlag(inst.flop),
@@ -5185,7 +5349,15 @@ Organization : OptiHK Limited
},
});
});
nodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
// Strip port entries from elements that duplicate page-port already created from pins above.
const elementsWithoutPorts = doc.elements
? Object.fromEntries(Object.entries(doc.elements).filter(([elName, el]) => !(el && el.type === 'port' && nodeNameMap[elName])))
: undefined;
nodes.push(...buildElementNodesFromYaml(
elementsWithoutPorts ? { ...doc, elements: elementsWithoutPorts } : doc,
usesGdsYUp,
nodeNameMap
));
forEachBundleLink(doc, (bundleName, bundle, link) => {
const route = createRouteSettings(manifest, { ...bundle, ...link, bundle_group: bundleName });
@@ -5217,6 +5389,9 @@ Organization : OptiHK Limited
}
});
// Restore the previous origin after parsing.
if (docOrigin) setExportOrigin(savedOrigin.x, savedOrigin.y);
return {
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
name: doc.name || cellName,
@@ -5224,7 +5399,8 @@ Organization : OptiHK Limited
canvasSize: normalizeCanvasSize(doc.canvas_size || doc.canvasSize),
nodes,
edges,
port: pagePort
port: pagePort || { x: 0, y: 0, a: 0 },
origin: docOrigin || undefined,
};
};
@@ -5272,7 +5448,8 @@ Organization : OptiHK Limited
const pdkNames = [...new Set(allNodes
.filter(n => n.data?.componentName && !n.data?.elementType
&& !isForgeComponent(n.data.componentName)
&& !isBasicComponent(n.data.componentName))
&& !(n.data?.type === 'composite')
&& !(n.data?.boxSize && n.data?.ports))
.map(n => n.data.componentName))];
if (pdkNames.length > 0) {
const metaResults = await Promise.all(
@@ -5289,7 +5466,7 @@ Organization : OptiHK Limited
return {
...node,
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz),
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '' }
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '', category: node.data.category || metadata.__category__ || '' }
};
});
}
@@ -5297,8 +5474,14 @@ Organization : OptiHK Limited
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
// Respect closed-tab state so tabs closed before refresh stay closed.
let closedTabNames;
try { closedTabNames = JSON.parse(localStorage.getItem('mxpic-closed-tabs') || '[]'); } catch { closedTabNames = []; }
const resolvedProjectPage = loadedProjectPage || projectPage;
setPages([resolvedProjectPage, ...nonProjectPages]);
setPages([
resolvedProjectPage,
...nonProjectPages.map(p => closedTabNames.includes(p.name) ? { ...p, isClosed: true } : p)
]);
setActivePageId(resolvedProjectPage.id);
setProjectCompositeMap({ [currentProjectName]: nonProjectPages.map(page => page.name) });
setStandaloneComposites([]);
@@ -5374,6 +5557,7 @@ Organization : OptiHK Limited
useEffect(() => {
const missingPortNodes = [];
pages.forEach(page => {
page.nodes.forEach(node => {
const componentName = node.data?.componentName;
@@ -5461,6 +5645,13 @@ Organization : OptiHK Limited
// Open an existing project page by name.
const openProject = useCallback((name) => {
// Re-opening a previously closed tab — remove from closed list.
setClosedTabs(current => {
if (!current.includes(name)) return current;
const next = current.filter(n => n !== name);
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
return next;
});
setPages(prev => {
const existing = prev.find(p => p.name === name && p.type === 'project');
if (existing) {
@@ -5471,10 +5662,20 @@ Organization : OptiHK Limited
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
name: name,
type: 'project',
nodes: [],
nodes: [
{
id: 'page-port',
type: 'portNode',
position: { x: 600, y: 4400 },
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
draggable: true,
selectable: true,
deletable: false,
}
],
edges: [],
canvasSize: DEFAULT_CANVAS_SIZE,
port: { x: 0, y: 0, a: 0 }
port: { x: 600, y: 4400, a: 0 }
};
setActivePageId(newProjectPage.id);
setProjectCompositeMap(prevMap => ({ ...prevMap, [name]: prevMap[name] || [] }));
@@ -5484,6 +5685,12 @@ Organization : OptiHK Limited
// Open a canvas tab and make it active.
const openPage = useCallback((name) => {
setClosedTabs(current => {
if (!current.includes(name)) return current;
const next = current.filter(n => n !== name);
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
return next;
});
const belongsToProject = Object.values(projectCompositeMap).some(comps => comps.includes(name));
if (!belongsToProject && !standaloneComposites.includes(name)) {
setStandaloneComposites(prev => [...prev, name]);
@@ -5504,7 +5711,7 @@ Organization : OptiHK Limited
{
id: 'page-port',
type: 'portNode',
position: { x: 50, y: 150 },
position: { x: 600, y: 4400 },
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
draggable: true,
selectable: true,
@@ -5512,7 +5719,7 @@ Organization : OptiHK Limited
}
],
edges: [],
port: { x: 50, y: 150, a: 0 }
port: { x: 600, y: 4400, a: 0 }
};
setActivePageId(newComposite.id);
return [...prev, newComposite];
@@ -5596,7 +5803,7 @@ Organization : OptiHK Limited
{
id: 'page-port',
type: 'portNode',
position: { x: 50, y: 150 },
position: { x: 600, y: 4400 },
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: 0, width: 0.5, layer: 'WG_CORE', description: '' },
draggable: true,
selectable: true,
@@ -5604,7 +5811,7 @@ Organization : OptiHK Limited
}
],
edges: [],
port: { x: 50, y: 150, a: 0 }
port: { x: 600, y: 4400, a: 0 }
};
setPages(prev => [...prev, newCell]);
@@ -5619,6 +5826,15 @@ Organization : OptiHK Limited
// Close a canvas tab without deleting its saved content.
const closePage = useCallback((pageId) => {
setPages(prev => {
const target = prev.find(p => p.id === pageId);
if (target) {
setClosedTabs(current => {
if (current.includes(target.name)) return current;
const next = [...current, target.name];
localStorage.setItem('mxpic-closed-tabs', JSON.stringify(next));
return next;
});
}
const closed = prev.map(p => p.id === pageId ? { ...p, isClosed: true } : p);
if (activePageId === pageId) {
const idx = prev.findIndex(p => p.id === pageId);
@@ -5958,10 +6174,7 @@ Organization : OptiHK Limited
});
return;
}
const componentDisplayName = generateComponentDisplayName(parsedData.category || parsedData.name, {
singularize: Boolean(parsedData.category),
abbreviate: Boolean(parsedData.category)
});
const componentDisplayName = generateComponentDisplayName(parsedData.category || parsedData.name);
const newNode = {
id: Date.now().toString(),
type: 'rotatableNode',
@@ -6402,8 +6615,11 @@ coordinate_system: gds_y_up
canvas_size:
width: ${Number(page.canvasSize?.width || DEFAULT_CANVAS_SIZE.width)}
height: ${Number(page.canvasSize?.height || DEFAULT_CANVAS_SIZE.height)}
project: ${currentProjectName}
name: ${page.name}
origin:
x: ${Number(canvasOrigin.x).toFixed(1)}
y: ${Number(canvasOrigin.y).toFixed(1)}
project: "${currentProjectName}"
name: "${page.name}"
type: ${page.type === 'project' ? 'project' : 'composite'}
version: "1.0.0"
@@ -6430,7 +6646,7 @@ ${instancesBlock}
${elementsBlock}
${bundlesBlock}`;
}, [currentProjectName, library, buildBundlesYaml]);
}, [currentProjectName, library, canvasOrigin.x, canvasOrigin.y, buildBundlesYaml]);
// Open or refresh a tab showing the generated SVG layout preview.
const openLayoutPreview = useCallback((cellName, svgUrl, layoutBounds) => {
@@ -6590,14 +6806,6 @@ ${bundlesBlock}`;
return (
<div style={{ display: 'flex', width: '100%', height: '100%', userSelect: dragging ? 'none' : 'auto' }}>
<div className="site-nav-actions">
<button className="mini-btn" onClick={() => { window.location.href = '/dashboard'; }}>
Dashboard
</button>
<button className="mini-btn" onClick={() => { window.location.href = '/logout'; }}>
Logout
</button>
</div>
<LeftPanel
projectTreeItems={projectTreeItems}
library={libraryWithCells} treeKey={treeKey} expanded={expanded}
@@ -6634,6 +6842,14 @@ ${bundlesBlock}`;
<button onClick={(e) => { e.stopPropagation(); closePage(page.id); }}>x</button>
</div>
))}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
<button className="mini-btn" onClick={() => { window.location.href = '/dashboard'; }}>
⊞ Dashboard
</button>
<button className="mini-btn" onClick={() => { window.location.href = '/logout'; }}>
↩ Logout
</button>
</div>
</div>
<div
ref={canvasViewportRef}
@@ -6774,7 +6990,7 @@ ${bundlesBlock}`;
onNodeMouseDown={onNodeMouseDown}
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
onNodeMouseUp={clearSpaceRotateNode}
nodeTypes={{ rotatableNode: RotatableNode, portNode: PortNode, anchorNode: AnchorNode, canvasBoundaryNode: CanvasBoundaryNode, rulerPointNode: RulerPointNode, rulerMeasurementNode: RulerMeasurementNode }}
nodeTypes={{ rotatableNode: RotatableNode, portNode: PortNode, anchorNode: AnchorNode, canvasBoundaryNode: CanvasBoundaryNode, originMarkerNode: OriginMarkerNode, rulerPointNode: RulerPointNode, rulerMeasurementNode: RulerMeasurementNode }}
edgeTypes={edgeTypes}
nodeExtent={canvasNodeExtent}
snapToGrid={gridSnap}
@@ -6784,7 +7000,7 @@ ${bundlesBlock}`;
elementsSelectable={true}
connectionMode="loose"
connectionRadius={50}
minZoom={0.02}
minZoom={canvasMinZoom}
maxZoom={4}
defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}
onMoveEnd={handleCanvasViewportMoveEnd}
@@ -6817,6 +7033,7 @@ ${bundlesBlock}`;
projectName={currentProjectName}
compositeNames={compositePageNames}
width={rightWidth}
canvasOrigin={canvasOrigin}
onRenameComponent={renameComponent}
onUpdateNode={handleUpdateNode}
onUpdateEdgeRoute={handleUpdateEdgeRoute}
+24 -1
View File
@@ -8,4 +8,27 @@
4.Fixed the abnormal port shift after rotation.
5.Fixed the abnormal position of individual ports.
5.Fixed the abnormal position of individual ports.
06/15
Enhanced the contrast between the inside and outside of the canvas border.
Capped the maximum amount by which the canvas can be scaled down (set a minimum canvas size limit).
Labels/tags, once closed, will no longer be reloaded after a page refresh.
Modified the "select origin" functionality and set the default origin to the canvas position (500, 500).
Fixed a bug where the instance name counter was shared across different canvases within the same project.
06/16
Fixed a bug where component icons would not load when importing a YML file.
Added origin coordinates to the YML file; fixed a bug that caused component misalignment when opening a YML file with a different origin.
Fixed a bug where the default port would reload on every page refresh.