diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc index 43fd283..e2b00e6 100644 Binary files a/backend/__pycache__/database.cpython-39.pyc and b/backend/__pycache__/database.cpython-39.pyc differ diff --git a/backend/__pycache__/gds_builder.cpython-39.pyc b/backend/__pycache__/gds_builder.cpython-39.pyc index 7559ede..16b82fc 100644 Binary files a/backend/__pycache__/gds_builder.cpython-39.pyc and b/backend/__pycache__/gds_builder.cpython-39.pyc differ diff --git a/backend/__pycache__/layout_preview.cpython-39.pyc b/backend/__pycache__/layout_preview.cpython-39.pyc index 5d31222..008915a 100644 Binary files a/backend/__pycache__/layout_preview.cpython-39.pyc and b/backend/__pycache__/layout_preview.cpython-39.pyc differ diff --git a/backend/__pycache__/pdk_access.cpython-39.pyc b/backend/__pycache__/pdk_access.cpython-39.pyc index b16ac8f..2113cae 100644 Binary files a/backend/__pycache__/pdk_access.cpython-39.pyc and b/backend/__pycache__/pdk_access.cpython-39.pyc differ diff --git a/backend/__pycache__/pdk_registry.cpython-39.pyc b/backend/__pycache__/pdk_registry.cpython-39.pyc index ff29709..33473b9 100644 Binary files a/backend/__pycache__/pdk_registry.cpython-39.pyc and b/backend/__pycache__/pdk_registry.cpython-39.pyc differ diff --git a/backend/__pycache__/routed_layout_preview.cpython-39.pyc b/backend/__pycache__/routed_layout_preview.cpython-39.pyc index 4d0ad2f..2caad5e 100644 Binary files a/backend/__pycache__/routed_layout_preview.cpython-39.pyc and b/backend/__pycache__/routed_layout_preview.cpython-39.pyc differ diff --git a/backend/__pycache__/technology_manifest.cpython-39.pyc b/backend/__pycache__/technology_manifest.cpython-39.pyc index 8220613..72b64b4 100644 Binary files a/backend/__pycache__/technology_manifest.cpython-39.pyc and b/backend/__pycache__/technology_manifest.cpython-39.pyc differ diff --git a/database/admin/layout/mxpic_project_1/mxpic_project_1.svg b/database/admin/layout/mxpic_project_1/mxpic_project_1.svg deleted file mode 100644 index 39e4fad..0000000 --- a/database/admin/layout/mxpic_project_1/mxpic_project_1.svg +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/database/admin/layout/mxpic_project_1/mxpic_project_1.yml b/database/admin/layout/mxpic_project_1/mxpic_project_1.yml deleted file mode 100644 index b021f2a..0000000 --- a/database/admin/layout/mxpic_project_1/mxpic_project_1.yml +++ /dev/null @@ -1,117 +0,0 @@ -# ============================================= -# mxPIC Cell/Project Definition File -# ============================================= -schema_version: "2.0.0" -kind: cell -coordinate_system: gds_y_up -canvas_size: - width: 5000 - height: 5000 -project: mxpic_project_1 -name: mxpic_project_1 -type: project -version: "1.0.0" - -# 1. External Ports (How this cell connects to the outside world) -ports: -- name: port - layer: WG_CORE - x: 50.0 - y: -150.0 - angle: 0.0 - width: 0.5 - -# 2. Instances (The sub-components dropped onto this canvas) -instances: - component_1: - component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 - x: 100.0 - y: -2290.0 - rotation: 0.0 - flip: 0 - flop: 0 - mirror: false - settings: - length: - - component_4: - component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 - x: 100.0 - y: -1970.0 - rotation: 0.0 - flip: 0 - flop: 0 - mirror: false - settings: - length: - - component_2: - component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2 - x: 100.0 - y: -2560.0 - rotation: 0.0 - flip: 0 - flop: 0 - mirror: false - settings: - length: - -elements: - port: - type: port - x: 50.0 - y: -150.0 - angle: 0.0 - layer: WG_CORE - width: 0.5 - description: "" - anchor_1: - type: anchor - x: 120.0 - y: -2150.0 - angle: 0.0 - layer: WG_CORE - width: 0.5 - description: "" - anchor_2: - type: anchor - x: 130.0 - y: -2430.0 - angle: 0.0 - layer: WG_CORE - width: 0.5 - description: "" - -# 3. Bundles (Grouped links for multi-bus/parallel routing) -bundles: - output_bus: - routing_type: euler_bend - links: - - from: anchor_1:right - to: component_4:b2 - xsection: strip - family: optical - width: 0.45 - radius: 10 - routing_type: euler_bend - - from: anchor_1:left - to: component_1:a1 - xsection: strip - family: optical - width: 0.45 - radius: 10 - routing_type: euler_bend - - from: component_1:b2 - to: anchor_2:right - xsection: strip - family: optical - width: 0.45 - radius: 10 - routing_type: euler_bend - - from: anchor_2:left - to: component_2:a1 - xsection: strip - family: optical - width: 0.45 - radius: 10 - routing_type: euler_bend \ No newline at end of file diff --git a/database/mxpic_data.db b/database/mxpic_data.db index 9d36722..c9a734f 100644 Binary files a/database/mxpic_data.db and b/database/mxpic_data.db differ diff --git a/frontend/canvas.html b/frontend/canvas.html index 63eec67..bf06dfa 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -3646,6 +3646,10 @@ if (!activePageId) return; const relevantChanges = changes.filter(change => change.id !== '__canvas-boundary__'); if (relevantChanges.length === 0) return; + const removedNodeIds = new Set(relevantChanges.filter(change => change.type === 'remove').map(change => change.id)); + if (removedNodeIds.size > 0 && activePage) { + releaseComponentDisplayNames(activePage.nodes.filter(node => removedNodeIds.has(node.id))); + } setPages(prev => prev.map(p => { if (p.id !== activePageId) return p; const newNodes = applyNodeChanges(relevantChanges, p.nodes).map(node => { @@ -3667,7 +3671,7 @@ } return { ...p, nodes: newNodes, port: newPort }; })); - }, [activePageId, activeCanvasSize]); + }, [activePageId, activePage, activeCanvasSize]); const onEdgesChange = useCallback((changes) => { if (!activePageId) return; @@ -3812,6 +3816,7 @@ const selectedNodes = activePage.nodes.filter(n => n.selected); if (selectedNodes.length > 0) { setClipboard({ nodes: JSON.parse(JSON.stringify(selectedNodes)) }); + releaseComponentDisplayNames(selectedNodes); const selectedNodeIds = new Set(selectedNodes.map(n => n.id)); const newNodes = activePage.nodes.filter(n => !selectedNodeIds.has(n.id)); const newEdges = activePage.edges.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)); @@ -3822,7 +3827,13 @@ const handlePaste = useCallback(() => { if (!activePage || clipboard.nodes.length === 0) return; const newNodes = clipboard.nodes.map(node => { - const copiedName = generateComponentDisplayName(); + const copyCategory = node.data?.libraryCategory && node.data.libraryCategory !== 'basic' + ? node.data.libraryCategory + : (node.data?.category && node.data.category !== 'basic' ? node.data.category : ''); + const copiedName = generateComponentDisplayName( + copyCategory || node.data?.componentName || node.data?.elementType, + { singularize: Boolean(copyCategory), abbreviate: Boolean(copyCategory) } + ); const copiedData = { ...node.data, componentDisplayName: copiedName @@ -3852,6 +3863,7 @@ const selectedNodes = activePage.nodes.filter(n => n.selected); const selectedNodeIds = new Set(selectedNodes.map(n => n.id)); if (selectedNodeIds.size > 0) { + releaseComponentDisplayNames(selectedNodes); const newNodes = activePage.nodes.filter(n => !selectedNodeIds.has(n.id)); const newEdges = activePage.edges.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)); setPages(prev => prev.map(p => p.id === activePage.id ? { ...p, nodes: newNodes, edges: newEdges } : p)); @@ -3899,16 +3911,122 @@ }; }, [handleCopy, handleCut, handlePaste, handleDelete, rotateComponentByNinety, getSpaceRotationTarget, clearSpaceRotateNode]); - const componentCounterRef = useRef(1); + const componentIndexesByPrefixRef = useRef({}); - const generateComponentDisplayName = useCallback(() => { - const name = `component_${componentCounterRef.current}`; - componentCounterRef.current += 1; - return name; + const COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS = { + directional_coupler: 'DC', + directional_couplers: 'DC', + multimode_interferometer: 'MMI', + multimode_interferometers: 'MMI', + photodetector: 'PD', + photodetectors: 'PD', + waveguide: 'WG', + waveguides: 'WG', + transition: 'TRX', + transitions: 'TRX', + transistion: 'TRX', + transistions: 'TRX', + Mach_Zender_Modulator: 'MZM', + Mach_Zender_Modulators: 'MZM', + Mach_Zender_modulator: 'MZM', + Mach_Zender_modulators: 'MZM', + mach_zender_modulator: 'MZM', + mach_zender_modulators: 'MZM', + bending: 'BD', + bendings: 'BD', + edge_coupler: 'EC', + edge_couplers: 'EC', + grating_coupler: 'GC', + grating_couplers: 'GC', + termination: 'TERM', + terminations: 'TERM' + }; + + function parseComponentDisplayName(displayName) { + const match = String(displayName || '').match(/^(.+)_(\d+)$/); + if (!match) return null; + const index = Number(match[2]); + if (!Number.isInteger(index) || index < 1) return null; + return { prefix: match[1], index }; + } + + function reserveComponentDisplayName(displayName) { + const parsed = parseComponentDisplayName(displayName); + if (!parsed) return; + const usedIndexes = componentIndexesByPrefixRef.current[parsed.prefix] || new Set(); + usedIndexes.add(parsed.index); + componentIndexesByPrefixRef.current[parsed.prefix] = usedIndexes; + } + + function releaseComponentDisplayName(displayName) { + const parsed = parseComponentDisplayName(displayName); + if (!parsed) return; + const usedIndexes = componentIndexesByPrefixRef.current[parsed.prefix]; + if (!usedIndexes) return; + usedIndexes.delete(parsed.index); + if (usedIndexes.size === 0) { + delete componentIndexesByPrefixRef.current[parsed.prefix]; + } + } + + function releaseComponentDisplayNames(nodes = []) { + nodes.forEach(node => releaseComponentDisplayName(node?.data?.componentDisplayName)); + } + + function reserveComponentDisplayNamesFromPages() { + pages.forEach(page => { + (page.nodes || []).forEach(node => reserveComponentDisplayName(node?.data?.componentDisplayName)); + }); + } + + const normalizeComponentDisplayNamePrefix = useCallback((prefixSource, options = {}) => { + const cleanedPrefix = String(prefixSource || 'element') + .trim() + .replace(/[\\/]+/g, '_') + .replace(/\s+/g, '_') + .replace(/[^A-Za-z0-9_]+/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + if (!cleanedPrefix) return 'element'; + const abbreviation = options.abbreviate + ? COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS[cleanedPrefix] || COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS[cleanedPrefix.toLowerCase()] + : ''; + if (abbreviation) return abbreviation; + if (!options.singularize) return cleanedPrefix; + const parts = cleanedPrefix.split('_'); + const lastIndex = parts.length - 1; + const last = parts[lastIndex]; + if (last.length > 3 && last.endsWith('ies')) { + parts[lastIndex] = `${last.slice(0, -3)}y`; + } else if (last.length > 1 && last.endsWith('s')) { + parts[lastIndex] = last.slice(0, -1); + } + const singularPrefix = parts.join('_') || 'element'; + if (options.abbreviate) { + return COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS[singularPrefix] || COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS[singularPrefix.toLowerCase()] || singularPrefix; + } + return singularPrefix; }, []); + const generateComponentDisplayName = useCallback((prefixSource = 'element', options = {}) => { + const prefix = normalizeComponentDisplayNamePrefix(prefixSource, options); + reserveComponentDisplayNamesFromPages(); + const usedIndexes = componentIndexesByPrefixRef.current[prefix] || new Set(); + let nextIndex = 1; + while (usedIndexes.has(nextIndex)) nextIndex += 1; + const name = `${prefix}_${nextIndex}`; + usedIndexes.add(nextIndex); + componentIndexesByPrefixRef.current[prefix] = usedIndexes; + return name; + }, [normalizeComponentDisplayNamePrefix, pages]); + const renameComponent = useCallback((nodeId, newComponentDisplayName) => { if (!activePageId) return; + const oldDisplayName = activePage?.nodes.find(node => node.id === nodeId)?.data?.componentDisplayName; + if (oldDisplayName !== newComponentDisplayName) { + releaseComponentDisplayName(oldDisplayName); + reserveComponentDisplayName(newComponentDisplayName); + } setPages(prev => prev.map(p => { if (p.id !== activePageId) return p; return { @@ -3916,7 +4034,7 @@ nodes: p.nodes.map(n => n.id === nodeId ? { ...n, data: { ...n.data, componentDisplayName: newComponentDisplayName } } : n) }; })); - }, [activePageId]); + }, [activePageId, activePage]); const fetchLibrary = useCallback(async () => { try { @@ -4788,8 +4906,7 @@ const componentName = parsedData.componentName || parsedData.name; const basicArguments = createBasicSettings(componentName, parsedData.settings); const metadata = getBasicComponentMetadata(componentName, basicArguments); - const componentDisplayName = `${componentName.replace(/\s+/g, '_')}_${componentCounterRef.current}`; - componentCounterRef.current += 1; + const componentDisplayName = generateComponentDisplayName(componentName); const newNode = { id: Date.now().toString(), type: 'rotatableNode', @@ -4815,8 +4932,7 @@ return; } if (parsedData.type === 'element') { - const elementName = parsedData.elementType === 'anchor' ? `anchor_${componentCounterRef.current}` : `port_${componentCounterRef.current}`; - componentCounterRef.current += 1; + const elementName = generateComponentDisplayName(parsedData.elementType === 'anchor' ? 'anchor' : 'port'); const isPort = parsedData.elementType === 'port'; const newNode = isPort ? { @@ -4876,7 +4992,10 @@ return; } const selectedIsForge = isForgeComponent(selectedComponent); - const componentDisplayName = generateComponentDisplayName(); + const componentDisplayName = generateComponentDisplayName(parsedData.category || selectedComponent, { + singularize: Boolean(parsedData.category), + abbreviate: Boolean(parsedData.category) + }); const newNode = { id: Date.now().toString(), type: 'rotatableNode', @@ -4904,7 +5023,10 @@ }); return; } - const componentDisplayName = generateComponentDisplayName(); + const componentDisplayName = generateComponentDisplayName(parsedData.category || parsedData.name, { + singularize: Boolean(parsedData.category), + abbreviate: Boolean(parsedData.category) + }); const newNode = { id: Date.now().toString(), type: 'rotatableNode', diff --git a/tests/layout-ui-wiring.test.js b/tests/layout-ui-wiring.test.js index c4b5b7a..8cc46f6 100644 --- a/tests/layout-ui-wiring.test.js +++ b/tests/layout-ui-wiring.test.js @@ -197,6 +197,32 @@ assert( canvasHtml.includes('getSpaceRotationTarget') && canvasHtml.includes('selectedSpaceNode'), 'Space rotation should also use the currently selected component when no mouse-hold target is active' ); +assert( + canvasHtml.includes('const componentIndexesByPrefixRef = useRef({});') && + canvasHtml.includes('const usedIndexes = componentIndexesByPrefixRef.current[prefix] || new Set();') && + canvasHtml.includes('while (usedIndexes.has(nextIndex)) nextIndex += 1;') && + canvasHtml.includes('usedIndexes.add(nextIndex);') && + canvasHtml.includes('const name = `${prefix}_${nextIndex}`;') && + canvasHtml.includes('releaseComponentDisplayNames(selectedNodes);') && + canvasHtml.includes('releaseComponentDisplayName(oldDisplayName);') && + canvasHtml.includes('reserveComponentDisplayName(newComponentDisplayName);') && + !canvasHtml.includes('componentCounterRef.current') && + !canvasHtml.includes('componentCountersByPrefixRef') && + canvasHtml.includes('COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS') && + canvasHtml.includes("directional_coupler: 'DC'") && + canvasHtml.includes("multimode_interferometers: 'MMI'") && + canvasHtml.includes("photodetectors: 'PD'") && + canvasHtml.includes("waveguides: 'WG'") && + canvasHtml.includes("transitions: 'TRX'") && + canvasHtml.includes("Mach_Zender_modulators: 'MZM'") && + canvasHtml.includes("bendings: 'BD'") && + canvasHtml.includes("edge_couplers: 'EC'") && + canvasHtml.includes("grating_couplers: 'GC'") && + canvasHtml.includes("terminations: 'TERM'") && + canvasHtml.includes('abbreviate: Boolean(parsedData.category)') && + canvasHtml.includes('abbreviate: Boolean(copyCategory)'), + 'new PDK component instances should use their component category abbreviation as the display-name prefix' +); assert( canvasHtml.includes('normalizeAngle,') && canvasHtml.includes('normalizeAngle(Number(node.data?.rotation || 0) + 90)'), 'Space rotation should import normalizeAngle before using it'