upadate
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||