diff --git a/GDS_SVG_GENERATION_LOGIC.md b/GDS_SVG_GENERATION_LOGIC.md new file mode 100644 index 0000000..b1f106b --- /dev/null +++ b/GDS_SVG_GENERATION_LOGIC.md @@ -0,0 +1,216 @@ +# GDS and SVG Generation Logic Path + +This document traces the current code path for generating saved layout YAML, +layout preview SVG, and downloadable project GDS when the user clicks +`Build Layout` or `Build GDS`. + +Line numbers refer to the files as currently checked in. + +## Build-Time Router Gate + +`python backend/server.py` -> `app.run` starts without importing +`mxpic_router`, `mxpic_forge`, Nazca, or gdstk. Login, dashboard, canvas +editing, YAML generation, PDK browsing, and project save without preview do not +require the external build stack. + +Build actions validate the external stack on demand: + +- `Build GDS` -> `backend/gds_builder.py` -> `require_router_stack()`. +- `Build Layout` SVG preview -> `backend/routed_layout_preview.py` -> + `require_router_stack(require_gdstk=True)`. + +Important functions: + +- `ensure_router_path` (`backend/router_dependency.py` line 23) adds the sibling + `../mxpic_router` checkout to `sys.path` when present. +- `require_router_stack` (`backend/router_dependency.py` line 31) imports + `mxpic_router`, `nazca`, and the route backend used by `mxpic_router`. + `gdstk` is checked only when SVG preview generation requests it. + +## Generated Files + +- Saved cell YAML: `database//layout//.yml` + - Path helpers: `user_layout_root`, `project_root`, `cell_file_path` + (`backend/server.py` lines 124-137). +- Saved layout preview SVG: `database//layout//.svg` + - Path helper: `cell_svg_path` (`backend/server.py` lines 140-142). +- Optional route sidecar: `database//layout//.routes.yml` + - Path helper and writer: `cell_routes_path`, `write_route_points_sidecar` + (`backend/server.py` lines 145-175). +- Downloadable GDS export: `database/_exports//.gds` + - Created by `create_export_path` (`backend/pdk_access.py` lines 53-59). + +## Build Layout: Click Button -> YAML -> Router GDS -> SVG + +Compact path: + +`Click Build Layout` -> `handleBuildLayout` (`frontend/canvas.html` line 6209) : +`buildYamlForPage` (`frontend/canvas.html` line 6141) generates layout YAML -> +POST `/api/save-layout` (`frontend/canvas.html` line 6219) -> `save_layout` +(`backend/server.py` line 715) writes `.yml` -> `create_routed_layout_svg` +(`backend/server.py` line 734) -> `mxpic_router.build_project_gds` generates +temporary Nazca GDS -> `gdstk.read_gds` + `write_svg` writes `.svg` -> +backend returns `svg_url` -> `openLayoutPreview` (`frontend/canvas.html` line +6183) opens the SVG preview tab. + +Detailed path: + +1. The button is rendered only for non-preview pages: + - ` + // Renders recursive project/cell/instance navigation with open, drag, rename, and delete actions. + const ProjectTreeNode = ({ name, children, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas }) => { + if (children && children.__type__ === 'project') { + const projectName = children.__name__ || name; + const composites = children.composites || []; + const handleDoubleClick = () => { + if (onOpenProject) onOpenProject(projectName); + }; + return ( +
+ + + + {name} + > + + + {composites.map(comp => ( + + ))} +
+ ); + } + + if (children && children.__type__ === 'instance') { + const instanceName = children.__instance__ || name; + const pageName = children.__page__; + return ( +
onSelectInstance && onSelectInstance(pageName, instanceName)} + > + [] + {instanceName}
-
- {library && Object.keys(library).length > 0 ? ( - Object.entries(library).map(([key, value]) => ( - + ); + } + + if (children && children.__type__ === 'composite') { + const compositeName = children.__name__ || name; + const cellName = children.__cellName__ || compositeName; + const tree = children.tree || {}; + const handleDragStart = (event) => { + const dragData = JSON.stringify({ name: cellName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ }); + event.dataTransfer.setData('application/reactflow', dragData); + event.dataTransfer.effectAllowed = 'move'; + }; + const handleOpen = (event) => { + if (event.target.closest('.tree-expander')) return; + if (event.target.closest('.tree-delete-btn')) return; + if (onOpenComposite) onOpenComposite(cellName); + }; + const handleDeleteCanvas = (event) => { + event.preventDefault(); + event.stopPropagation(); + if (onDeleteCanvas) onDeleteCanvas(cellName); + }; + return ( +
+ + + + onOpenComposite && onOpenComposite(cellName)} + /> + + > + + + {Object.keys(tree).length > 0 ? ( + Object.entries(tree).map(([childName, childData]) => ( + )) ) : ( -

Loading library...

+
No components
)} -
- + + ); + } -
-
Routing modes
-
-
    -
  • Single mode wires
  • -
  • Multi-mode wires
  • -
  • DC electrical wires
  • -
  • RF electrical wires
  • -
+ if (children && children.__type__ === 'technology') { + return ( +
+ {name}: {children.description || '(empty)'}
-
- -
-
Session
-
-
Name: XXXXXX
-
ID: 12345678
- + ); + } + if (children && children.__type__ === 'block') { + return ( +
+ {name}: {children.description || '(empty)'}
-
- - ); + ); + } - const RightPanel = memo(({ selectedNode, width, onRenameComponent }) => { + const hasChildren = children && typeof children === 'object' && Object.keys(children).length > 0 && !children.__type__; + if (!hasChildren) { + return ( +
+ {name} +
+ ); + } + return ( +
+ + + + {name} + > + + + {Object.entries(children).map(([childName, childData]) => ( + + ))} +
+ ); + }; + + // Renders the nested contents of a composite cell inside the project tree. + const CompositeComponentTree = ({ name, children, canvasName, onSelectInstance }) => { + if (children && children.__type__ === 'component') { + const displayText = children.__instance__ || name; + + return ( +
onSelectInstance && onSelectInstance(canvasName, displayText)} + > + [] + {displayText} +
+ ); + } + if (children && typeof children === 'object' && !children.__type__) { + const hasChildren = Object.keys(children).length > 0; + return ( +
+ + + + {name} + > + + + {hasChildren && + Object.entries(children).map(([childName, childData]) => ( + + )) + } +
+ ); + } + return null; + }; + + // Renders project actions, canvas sizing controls, and the component library navigation. + const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas, onBuildGds, buildGdsBusy, onSaveProject, saveProjectBusy, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey, canvasSize, onCanvasSizeChange }) => { + const [projectPanelHeight, setProjectPanelHeight] = useState(270); + const [resizingProjectPanel, setResizingProjectPanel] = useState(false); + const leftPanelRef = useRef(null); + const size = normalizeCanvasSize(canvasSize); + + useEffect(() => { + if (!resizingProjectPanel) return; + const onMouseMove = (event) => { + if (!leftPanelRef.current) return; + const rect = leftPanelRef.current.getBoundingClientRect(); + const nextHeight = event.clientY - rect.top - 12; + setProjectPanelHeight(Math.min(620, Math.max(150, nextHeight))); + }; + const onMouseUp = () => setResizingProjectPanel(false); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, [resizingProjectPanel]); + + // Toggle the expanded state of the project tree panel. + const handleProjectToggle = () => { + onProjectToggle(); + }; + + const handleLibraryToggle = () => { + onToggle(); + }; + + return ( + + ); + }; + + // Renders editable properties for selected nodes, ports, anchors, and routes. + const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => { const [componentData, setComponentData] = useState(null); const [loading, setLoading] = useState(false); const [enlarged, setEnlarged] = useState(null); - const { setNodes } = useReactFlow(); const [editingComponentName, setEditingComponentName] = useState(false); const [tempComponentName, setTempComponentName] = useState(''); const [localX, setLocalX] = useState(''); const [localY, setLocalY] = useState(''); const [localRotation, setLocalRotation] = useState(''); + const [editingTransformField, setEditingTransformField] = useState(null); + const MIXED_VALUE = '--'; + const selectedPositionNodes = useMemo( + () => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position), + [selectedNodes, selectedNode] + ); + const isMultiNodeSelection = selectedPositionNodes.length > 1; + const isPortRotationNode = useCallback((node) => ( + node?.id === 'page-port' || node?.type === 'portNode' || node?.data?.elementType === 'port' + ), []); + const getNodeRotationValue = useCallback((node) => ( + isPortRotationNode(node) ? (node.data?.angle ?? 0) : (node.data?.rotation ?? 0) + ), [isPortRotationNode]); + const getSharedNumericDisplay = useCallback((nodes, getValue) => { + if (!nodes.length) return ''; + const firstValue = Number(getValue(nodes[0])); + if (!Number.isFinite(firstValue)) return MIXED_VALUE; + const sameValue = nodes.every(node => { + const nextValue = Number(getValue(node)); + return Number.isFinite(nextValue) && Math.abs(nextValue - firstValue) < 0.0005; + }); + return sameValue ? firstValue.toFixed(3) : MIXED_VALUE; + }, []); + const getSharedTextDisplay = useCallback((nodes, getValue) => { + if (!nodes.length) return ''; + const firstValue = getValue(nodes[0]) || ''; + return nodes.every(node => String(getValue(node) || '') === String(firstValue)) ? firstValue : MIXED_VALUE; + }, []); + const clearMixedInput = useCallback((event, setter) => { + if (event.currentTarget.value === MIXED_VALUE) { + setter(''); + } + }, []); + const beginTransformInput = useCallback((event, field, setter) => { + setEditingTransformField(field); + clearMixedInput(event, setter); + }, [clearMixedInput]); useEffect(() => { const nodeId = selectedNode?.id; - if (!nodeId) { + if (!nodeId || isMultiNodeSelection) { setComponentData(null); setLoading(false); return; } const compName = selectedNode?.data?.componentName; + const selectedIsComposite = selectedNode?.data?.type === 'composite' || compositeNames.includes(compName); + if (selectedNode?.data?.elementType || selectedIsComposite || isBasicComponent(compName)) { + setComponentData(null); + setLoading(false); + return; + } if (!compName) { setComponentData(null); setLoading(false); return; } + if (isForgeComponent(compName)) { + setComponentData(null); + setLoading(false); + return; + } if (componentData && componentData.name === compName && componentData.nodeId === nodeId) return; setLoading(true); - fetch(`/api/component/${encodeURIComponent(compName)}`) - .then(r => r.json()) + fetch(`/api/component/${encodeURIComponent(compName)}?project=${encodeURIComponent(projectName || '')}`) + .then(r => { + if (!r.ok) throw new Error('Component metadata not found'); + return r.json(); + }) .then(data => { setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name }); + onUpdateNode(nodeId, { + data: { + ports: data.ports || {}, + boxSize: normalizeBoxSize(data), + foundry: data.foundry || '', + process: data.process || '' + } + }); setLoading(false); }) .catch(() => setLoading(false)); - }, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName]); + }, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName, isMultiNodeSelection, compositeNames, projectName, onUpdateNode]); useEffect(() => { - if (selectedNode) { - setLocalX(selectedNode.position.x.toFixed(3)); - setLocalY(selectedNode.position.y.toFixed(3)); - setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3)); + if (editingTransformField) return; + if (selectedPositionNodes.length > 0) { + setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x)); + setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y)); + setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue)); + return; } - }, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.id]); + setLocalX(''); + setLocalY(''); + setLocalRotation(''); + }, [selectedPositionNodes, getSharedNumericDisplay, getNodeRotationValue, editingTransformField]); const updatePosition = useCallback((id, axis, value) => { const val = parseFloat(value); if (isNaN(val)) return; - setNodes(nds => nds.map(n => n.id === id ? { ...n, position: { ...n.position, [axis]: val } } : n)); - }, [setNodes]); + if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) { + selectedPositionNodes.forEach(node => { + onUpdateNode(node.id, { position: { [axis]: val } }); + }); + return; + } + onUpdateNode(id, { position: { [axis]: val } }); + }, [onUpdateNode, selectedPositionNodes]); - const updateRotation = useCallback((id, value) => { + const updateRotation = useCallback((id, value, isPortNode = false) => { const val = parseFloat(value); if (isNaN(val)) return; const clamped = Math.min(180, Math.max(-180, val)); - setNodes(nds => nds.map(n => n.id === id ? { ...n, data: { ...n.data, rotation: clamped } } : n)); - }, [setNodes]); + if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) { + selectedPositionNodes.forEach(node => { + const dataField = isPortRotationNode(node) ? { angle: clamped } : { rotation: clamped }; + onUpdateNode(node.id, { data: dataField }); + }); + return; + } + const dataField = isPortNode || id === 'page-port' ? { angle: clamped } : { rotation: clamped }; + onUpdateNode(id, { data: dataField }); + }, [onUpdateNode, selectedPositionNodes, isPortRotationNode]); + + const commitTransformInput = useCallback((event, field, setter) => { + const rawValue = event.currentTarget.value; + const val = parseFloat(rawValue); + if (!isNaN(val) && selectedNode) { + if (field === 'x') { + updatePosition(selectedNode.id, 'x', val); + } else if (field === 'y') { + updatePosition(selectedNode.id, 'y', val); + } else { + updateRotation(selectedNode.id, val, isPortRotationNode(selectedNode)); + } + setter(val.toFixed(3)); + } else if (field === 'x') { + setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x)); + } else if (field === 'y') { + setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y)); + } else { + setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue)); + } + setEditingTransformField(null); + }, [selectedNode, selectedPositionNodes, updatePosition, updateRotation, isPortRotationNode, getSharedNumericDisplay, getNodeRotationValue]); + + const toggleComponentTransform = useCallback((key) => { + if (!selectedNode) return; + onUpdateNode(selectedNode.id, { data: { [key]: !Boolean(selectedNode.data?.[key]) } }); + }, [onUpdateNode, selectedNode]); const formatPort = (port) => { - if (!port) return '—'; - return `x:${port.x ?? '?'}, y:${port.y ?? '?'}, a:${port.a ?? '?'}, w:${port.width ?? '?'}`; + if (!port) return '-'; + const description = port.description ? ` ${port.description}` : ''; + return `(${port.x ?? '?'}, ${port.y ?? '?'}, ${port.a ?? '?'})${description}`; }; - const currentRotation = selectedNode?.data?.rotation ?? 0; const currentComponentDisplayName = selectedNode?.data?.componentDisplayName || ''; + const selectedComponentName = selectedNode?.data?.componentName || ''; + const availableComponentsFromNode = Array.isArray(selectedNode?.data?.availableComponents) + ? selectedNode.data.availableComponents.filter(Boolean) + : []; + const availableComponents = Array.from(new Set([...availableComponentsFromNode, selectedComponentName].filter(Boolean))); + const selectedIsVirtualElement = selectedNode?.data?.elementType === 'port' || selectedNode?.data?.elementType === 'anchor'; + const canChooseComponent = !selectedIsVirtualElement && availableComponentsFromNode.length > 0; + const forgeSelected = isForgeComponent(selectedComponentName); + const basicSelected = isBasicComponent(selectedComponentName); + const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null; + const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {}; + const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments); + const selectedIsPort = !isMultiNodeSelection && selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'); + const selectedIsAnchor = !isMultiNodeSelection && selectedNode?.data?.elementType === 'anchor'; + const selectedNodeBoxSize = !isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType + ? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE) + : null; + const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {}); + const sharedComponentName = isMultiNodeSelection + ? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentName || node.data?.elementType || node.type || '') + : ''; + const sharedDisplayName = isMultiNodeSelection + ? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentDisplayName || node.data?.label || node.id) + : ''; + + const selectedRouteEdges = selectedEdges.length > 0 ? selectedEdges : (selectedEdge ? [selectedEdge] : []); + if (selectedRouteEdges.length > 0) { + const routes = selectedRouteEdges.map(edge => createRouteSettings(technologyManifest, edge.data?.route)); + const selectedEdgeIds = selectedRouteEdges.map(edge => edge.id); + const firstRoute = routes[0]; + const mixedValue = (key) => routes.every(route => String(route[key]) === String(firstRoute[key])) ? firstRoute[key] : '__mixed__'; + const route = { + ...firstRoute, + xsection: mixedValue('xsection'), + family: mixedValue('family'), + width: mixedValue('width'), + radius: mixedValue('radius'), + routing_type: mixedValue('routing_type') + }; + const routingTypes = (technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).routing_types || ['euler_bend', 'standard_bend']; + return ( + + ); + } + + const updateForgeArgument = (key, value, type) => { + if (!selectedNode) return; + let nextValue = value; + if (type === 'number') { + nextValue = value === '' ? '' : Number(value); + } else if (type === 'boolean') { + nextValue = Boolean(value); + } + onUpdateNode(selectedNode.id, { + data: { + forgeArguments: { + ...forgeArguments, + [key]: nextValue + } + } + }); + }; + + const updateBasicArgument = (key, value) => { + if (!selectedNode || !basicSelected) return; + const numericValue = Number(value); + const nextArguments = { + ...basicArguments, + [key]: Number.isFinite(numericValue) && value !== '' ? numericValue : value + }; + const metadata = getBasicComponentMetadata(selectedComponentName, nextArguments); + onUpdateNode(selectedNode.id, { + data: { + basicArguments: nextArguments, + ports: metadata?.pins || metadata?.ports || {}, + boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize + } + }); + }; + + const updatePortField = (key, value, type = 'text') => { + if (!selectedNode) return; + let nextValue = type === 'number' ? Number(value || 0) : value; + if (key === 'portNumber') nextValue = Math.max(1, Math.floor(nextValue || 1)); + if (key === 'pitch') nextValue = Math.max(0, Number(nextValue || 0)); + const dataUpdate = { [key]: nextValue }; + if (key === 'portName') { + dataUpdate.componentDisplayName = value || selectedNode.data?.componentDisplayName; + dataUpdate.label = value || selectedNode.data?.label; + } + if (key === 'portNumber' || key === 'pitch' || key === 'width') { + const nextData = { ...selectedNode.data, ...dataUpdate }; + dataUpdate.ports = buildElementPorts(selectedNode.data?.elementType === 'anchor' ? 'anchor' : 'port', nextData); + dataUpdate.boxSize = buildElementBoxSize(nextData); + } + onUpdateNode(selectedNode.id, { data: dataUpdate }); + }; const handleStartEditName = () => { setTempComponentName(currentComponentDisplayName); @@ -456,70 +3160,78 @@ padding: 12, display: 'flex', flexDirection: 'column', height: '100%', boxSizing: 'border-box', overflowY: 'auto' }}> -
+
Transforms
{selectedNode ? (
- - setLocalX(e.target.value)} - onBlur={() => { - const val = parseFloat(localX); - if (!isNaN(val) && selectedNode) { - updatePosition(selectedNode.id, 'x', val); - setLocalX(val.toFixed(3)); - } else if (selectedNode) { - setLocalX(selectedNode.position.x.toFixed(3)); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur(); - }} - /> -

- - setLocalY(e.target.value)} - onBlur={() => { - const val = parseFloat(localY); - if (!isNaN(val) && selectedNode) { - updatePosition(selectedNode.id, 'y', val); - setLocalY(val.toFixed(3)); - } else if (selectedNode) { - setLocalY(selectedNode.position.y.toFixed(3)); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur(); - }} - /> -

- - setLocalRotation(e.target.value)} - onBlur={() => { - const val = parseFloat(localRotation); - if (!isNaN(val) && selectedNode) { - updateRotation(selectedNode.id, val); - setLocalRotation(val.toFixed(3)); - } else if (selectedNode) { - setLocalRotation(((selectedNode.data?.rotation || 0)).toFixed(3)); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur(); - }} - /> +
+ + + +
+ {selectedPositionNodes.length > 1 && ( +
+ {selectedPositionNodes.length} selected +
+ )} + {!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && ( +
+ + +
+ )}
) : (

Select a node to inspect

@@ -527,12 +3239,299 @@
- {selectedNode?.data?.componentName && ( -
-
Parameters
+ {isMultiNodeSelection && ( +
+
Component Information
- {loading ? ( -

Loading data...

+
+
Selection: {selectedPositionNodes.length} components
+
Name: {sharedDisplayName || MIXED_VALUE}
+
Component: {sharedComponentName || MIXED_VALUE}
+
+
+
+ )} + + {selectedIsPort && ( +
+
Port
+
+ + updatePortField('portName', event.target.value)} + /> +

+ + updatePortField('description', event.target.value)} + /> +

+ + updatePortField('layer', event.target.value)} + /> +

+ + updatePortField('width', event.target.value, 'number')} + /> +

+ + updatePortField('portNumber', event.target.value, 'number')} + /> +

+ + updatePortField('pitch', event.target.value, 'number')} + /> +
+
+ )} + + {selectedIsAnchor && ( +
+
Anchor
+
+ + onUpdateNode(selectedNode.id, { data: { componentDisplayName: event.target.value, label: event.target.value } })} + /> +

+ + onUpdateNode(selectedNode.id, { data: { description: event.target.value } })} + /> +

+ + updatePortField('portNumber', event.target.value, 'number')} + /> +

+ + updatePortField('pitch', event.target.value, 'number')} + /> +
+
+ )} + + {!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && ( +
+
Parameters
+
+ {canChooseComponent && ( +
+ + +
+ )} + {selectedNodeBoxSize && ( +
+ +
+
W {selectedNodeBoxSize.width.toFixed(3)} um
+
H {selectedNodeBoxSize.height.toFixed(3)} um
+
+
+ )} + {basicSelected && basicMetadata ? ( + <> +
+ + {editingComponentName ? ( + setTempComponentName(e.target.value)} + onBlur={handleSaveName} + onKeyDown={handleKeyDown} + autoFocus + /> + ) : ( +
+ {currentComponentDisplayName || selectedComponentName} + Edit +
+ )} +
+

Nazca Primitive:

+
+ {selectedComponentName} / {basicMetadata.process} +
+
+ {Object.entries(basicArguments).map(([key, value]) => ( + + ))} +
+

Ports:

+
    + {Object.entries(basicMetadata.ports || {}).map(([portName, portInfo]) => ( +
  • + {portName} : {formatPort(portInfo)} +
  • + ))} +
+ + ) : forgeSelected ? ( + <> +
+ + {editingComponentName ? ( + setTempComponentName(e.target.value)} + onBlur={handleSaveName} + onKeyDown={handleKeyDown} + autoFocus + /> + ) : ( +
+ {currentComponentDisplayName || selectedComponentName} + Edit +
+ )} +
+ +
+

+ Cell: {FORGE_COMPONENT_TYPE} +

+

+ Generator: mxpic_forge +

+
+ +

Forge Arguments:

+
+ {Object.entries(forgeArguments).map(([key, value]) => { + const isBoolean = typeof value === 'boolean'; + const isNumber = typeof value === 'number'; + return ( + + ); + })} +
+ + ) : loading ? ( +

Loading data...

) : componentData ? ( <>
@@ -540,7 +3539,7 @@ {editingComponentName ? ( setTempComponentName(e.target.value)} onBlur={handleSaveName} onKeyDown={handleKeyDown} @@ -564,31 +3563,31 @@ title="Click to edit" > {currentComponentDisplayName || componentData.name} - + Edit
)}
-
-

- Cell: {componentData.name} -

-

- Foundry: {componentData.foundry}
- Process: {componentData.process} -

+
+

+ Cell: {componentData.name} +

+

+ Foundry: {componentData.foundry}
+ Process: {componentData.process} +

-

Ports:

-