(function (root, factory) { const helpers = factory(); if (typeof module === 'object' && module.exports) { module.exports = helpers; } root.MxpicCanvasHelpers = helpers; })(typeof window !== 'undefined' ? window : globalThis, function () { const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge'; const FORGE_COMPONENT_TYPE = 'generate_with_forge'; const ELEMENT_COMPONENTS = { Port: { name: 'Port', elementType: 'port', ports: { port: { x: 0, y: 0, a: 0, width: 0.5 } } }, Anchor: { name: 'Anchor', elementType: 'anchor', ports: { left: { x: -20, y: 0, a: 180, width: 0.5 }, right: { x: 20, y: 0, a: 0, width: 0.5 } } } }; const DEFAULT_FORGE_ARGUMENTS = { function_name: 'straight', component_name: '', pdk: 'Silterra/EMO1_2ML_CU_Al_RDL', layer: 'WG_CORE', length: 100, width: 0.5, radius: 10, gap: 0.2, spacing: 10, angle: 0, wavelength: 1310, port_count: 2, include_heater: false, include_electrical_ports: false, notes: '' }; const createForgeArguments = (overrides) => ({ ...DEFAULT_FORGE_ARGUMENTS, ...(overrides || {}) }); const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE; const normalizeAngle = (angle) => { const value = Number(angle); if (!Number.isFinite(value)) return 0; let normalized = ((value % 360) + 360) % 360; if (normalized > 180) normalized -= 360; return Object.is(normalized, -0) ? 0 : normalized; }; const portSideFromAngle = (angle) => { const normalized = normalizeAngle(angle); if (normalized === 0) return 'right'; if (normalized === 180 || normalized === -180) return 'left'; if (normalized === 90) return 'top'; if (normalized === -90) return 'bottom'; return Math.abs(normalized) < 90 ? 'right' : 'left'; }; const roundPercent = (value) => Number(value.toFixed(3)); const scaledPercent = (value, min, max, invert) => { if (!Number.isFinite(value) || !Number.isFinite(min) || !Number.isFinite(max) || min === max) return null; const ratio = (value - min) / (max - min); const visualRatio = invert ? 1 - ratio : ratio; return roundPercent(15 + visualRatio * 70); }; const fallbackPercent = (index, count) => { if (count <= 1) return 50; return roundPercent(15 + (index / (count - 1)) * 70); }; const buildSideHandles = (ports, side) => { const vertical = side === 'left' || side === 'right'; const coordinate = vertical ? 'y' : 'x'; const values = ports.map(port => Number(port.info[coordinate])).filter(Number.isFinite); const min = values.length ? Math.min(...values) : null; const max = values.length ? Math.max(...values) : null; return ports.map((port, index) => { const physicalPercent = scaledPercent(Number(port.info[coordinate]), min, max, vertical); const percent = physicalPercent == null ? fallbackPercent(index, ports.length) : physicalPercent; const percentValue = `${percent}%`; const style = vertical ? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' } : { left: percentValue, transform: side === 'top' ? 'translate(-50%, -50%)' : 'translate(-50%, 50%)' }; return { name: port.name, position: side, style, port: port.info }; }); }; const buildPortHandles = (ports) => { const grouped = { left: [], right: [], top: [], bottom: [] }; Object.entries(ports || {}).forEach(([name, info]) => { if (name === 'a0' || name === 'b0') return; const side = portSideFromAngle(info && info.a); grouped[side].push({ name, info: info || {} }); }); Object.values(grouped).forEach(sidePorts => { sidePorts.sort((a, b) => { const sideA = portSideFromAngle(a.info.a); const vertical = sideA === 'left' || sideA === 'right'; const primary = vertical ? Number(b.info.y || 0) - Number(a.info.y || 0) : Number(a.info.x || 0) - Number(b.info.x || 0); return primary || a.name.localeCompare(b.name); }); }); return [ ...buildSideHandles(grouped.left, 'left'), ...buildSideHandles(grouped.right, 'right'), ...buildSideHandles(grouped.top, 'top'), ...buildSideHandles(grouped.bottom, 'bottom') ]; }; const toYamlScalar = (value) => { if (value === null || value === undefined) return '""'; if (typeof value === 'number' && Number.isFinite(value)) return String(value); if (typeof value === 'boolean') return value ? 'true' : 'false'; const numericValue = Number(value); if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(numericValue) && String(numericValue) === value.trim()) { return value.trim(); } return JSON.stringify(String(value)); }; const buildSettingsYaml = (settings, indent) => { const pad = ' '.repeat(indent); const entries = Object.entries(settings || {}); if (entries.length === 0) return `${pad}{}`; return entries.map(([key, value]) => `${pad}${key}: ${toYamlScalar(value)}`).join('\n'); }; const buildInstanceYaml = ({ instanceName, componentName, componentPath, position, rotation, forgeArguments }) => { const forge = isForgeComponent(componentName); const componentValue = forge ? FORGE_COMPONENT_TYPE : componentPath; const settings = forge ? createForgeArguments(forgeArguments) : null; const settingsYaml = forge ? `\n settings:\n${buildSettingsYaml(settings, 6)}` : '\n settings:\n length:'; return ` ${instanceName}: component: ${componentValue} x: ${Number(position.x || 0).toFixed(1)} y: ${Number(position.y || 0).toFixed(1)} rotation: ${Number(rotation || 0).toFixed(1)} mirror: false${settingsYaml}`; }; const buildInstancesYaml = ({ nodes, resolveComponentPath }) => { return (nodes || []) .filter(node => node.data && node.data.componentName && !node.data.elementType) .map(node => { const data = node.data; const componentName = data.componentName || ''; const componentPath = isForgeComponent(componentName) ? FORGE_COMPONENT_TYPE : (resolveComponentPath ? resolveComponentPath(componentName) : componentName); return buildInstanceYaml({ instanceName: data.componentDisplayName || node.id, componentName, componentPath, position: node.position || { x: 0, y: 0 }, rotation: data.rotation || 0, forgeArguments: data.forgeArguments }); }) .join('\n\n'); }; const getNodePortName = (node) => { const name = node && node.data && (node.data.portName || node.data.componentDisplayName || node.data.label); return name || (node && node.id) || 'port'; }; const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode'); const isElementNode = (node) => node && node.data && (node.data.elementType === 'port' || node.data.elementType === 'anchor'); const buildElementPorts = (elementType, data) => { const element = ELEMENT_COMPONENTS[elementType === 'anchor' ? 'Anchor' : 'Port']; if (!element) return {}; if (element.elementType === 'port') { return { port: { x: 0, y: 0, a: Number((data && (data.angle ?? data.a)) ?? 0), width: Number((data && data.width) || 0.5) } }; } return JSON.parse(JSON.stringify(element.ports)); }; const buildPageComponentPorts = (port, nodes) => { const portNodes = (nodes || []).filter(isPortElementNode); if (portNodes.length > 0) { return portNodes.reduce((ports, node) => { const data = node.data || {}; ports[getNodePortName(node)] = { x: Number((node.position && node.position.x) || 0), y: Number((node.position && node.position.y) || 0), a: Number(data.angle ?? data.a ?? 0), width: Number(data.width || 0.5) }; return ports; }, {}); } if (!port) return {}; return { port: { x: Number(port.x || 0), y: Number(port.y || 0), a: Number(port.a || 0), width: Number(port.width || 0.5) } }; }; const buildCanvasPortsYaml = (nodes, fallbackPort) => { const ports = buildPageComponentPorts(fallbackPort, nodes); const entries = Object.entries(ports); if (entries.length === 0) return 'ports: []'; const sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node])); const lines = entries.map(([name, info]) => { const data = (sourceNodes.get(name) && sourceNodes.get(name).data) || {}; const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : ''; return `- name: ${name} ${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'} x: ${Number(info.x || 0).toFixed(1)} y: ${Number(info.y || 0).toFixed(1)} angle: ${Number(info.a || 0).toFixed(1)} width: ${Number(info.width || 0.5)}${description}`; }); return `ports:\n${lines.join('\n')}`; }; const buildPortsYaml = (port) => buildCanvasPortsYaml([], port); const buildElementsYaml = (nodes) => { const elementNodes = (nodes || []).filter(isElementNode); if (elementNodes.length === 0) return 'elements: {}'; const lines = elementNodes.map(node => { const data = node.data || {}; const name = data.componentDisplayName || data.portName || node.id; const angle = data.elementType === 'port' ? data.angle : data.rotation; return ` ${name}: type: ${data.elementType} x: ${Number((node.position && node.position.x) || 0).toFixed(1)} y: ${Number((node.position && node.position.y) || 0).toFixed(1)} angle: ${Number(angle || 0).toFixed(1)} layer: ${data.layer || 'WG_CORE'} width: ${Number(data.width || 0.5)} description: ${toYamlScalar(data.description || '')}`; }); return `elements:\n${lines.join('\n')}`; }; return { FORGE_COMPONENT_LABEL, FORGE_COMPONENT_TYPE, ELEMENT_COMPONENTS, DEFAULT_FORGE_ARGUMENTS, createForgeArguments, isForgeComponent, normalizeAngle, portSideFromAngle, buildPortHandles, buildElementPorts, buildInstanceYaml, buildInstancesYaml, buildPageComponentPorts, buildCanvasPortsYaml, buildPortsYaml, buildElementsYaml, buildSettingsYaml, toYamlScalar }; });