Files
mxpic_EDA/frontend/canvas-helpers.js
T
2026-05-29 21:51:57 +08:00

894 lines
32 KiB
JavaScript

(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 DEFAULT_COMPONENT_BOX_SIZE = { width: 132, height: 82 };
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
const PORT_NODE_SIZE = 30;
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: 0, y: -PORT_NODE_SIZE / 2, a: 180, width: 0.5 },
right: { x: PORT_NODE_SIZE, y: -PORT_NODE_SIZE / 2, a: 0, width: 0.5 }
}
}
};
const BASIC_COMPONENTS = {
waveguide: {
name: 'waveguide',
category: 'basic',
settings: { length: 100, width: 0.5, xsection: 'strip' }
},
'90 bend': {
name: '90 bend',
category: 'basic',
settings: { radius: 10, width: 0.5, xsection: 'strip' }
},
'180 bend': {
name: '180 bend',
category: 'basic',
settings: { radius: 10, width: 0.5, xsection: 'strip' }
},
circle: {
name: 'circle',
category: 'basic',
settings: { radius: 10, width: 0.5, xsection: 'strip' }
},
cricle: {
name: 'cricle',
category: 'basic',
hidden: true,
settings: { radius: 10, width: 0.5, xsection: 'strip' }
},
taper: {
name: 'taper',
category: 'basic',
settings: { length: 50, width1: 0.5, width2: 1, xsection: 'strip' }
}
};
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 FALLBACK_TECHNOLOGY_MANIFEST = {
routing_types: ['euler_bend', 'standard_bend'],
defaults: {
xsection: 'strip',
family: 'optical',
width: 0.45,
radius: 10,
routing_type: 'euler_bend'
},
xsections: {
strip: { family: 'optical', default_width: 0.45, default_radius: 10 },
rib_low: { family: 'optical', default_width: 0.45, default_radius: 10 },
metal_1: { family: 'electrical', default_width: 5, default_radius: 10 },
metal_2: { family: 'electrical', default_width: 5, default_radius: 10 }
}
};
const createForgeArguments = (overrides) => ({
...DEFAULT_FORGE_ARGUMENTS,
...(overrides || {})
});
const getTechnologyManifest = (manifest) => manifest || FALLBACK_TECHNOLOGY_MANIFEST;
const getXsectionInfo = (xsection, manifest) => {
const technology = getTechnologyManifest(manifest);
return (technology.xsections && technology.xsections[xsection]) || technology.xsections.strip || {};
};
const createRouteSettings = (manifest, overrides) => {
const technology = getTechnologyManifest(manifest);
const defaults = technology.defaults || FALLBACK_TECHNOLOGY_MANIFEST.defaults;
const xsection = (overrides && overrides.xsection) || defaults.xsection || 'strip';
const xsectionInfo = getXsectionInfo(xsection, technology);
const family = (overrides && overrides.family) || xsectionInfo.family || defaults.family || 'optical';
return {
xsection,
family,
width: Number((overrides && overrides.width) ?? xsectionInfo.default_width ?? defaults.width ?? 0.45),
radius: Number((overrides && overrides.radius) ?? xsectionInfo.default_radius ?? defaults.radius ?? 10),
routing_type: (overrides && overrides.routing_type) || defaults.routing_type || 'euler_bend',
widthEdited: Boolean(overrides && overrides.widthEdited)
};
};
const updateRouteField = (route, key, value, manifest) => {
const current = createRouteSettings(manifest, route);
const numericFields = new Set(['width', 'radius']);
const nextValue = numericFields.has(key) ? Number(value || 0) : value;
return {
...current,
[key]: nextValue,
widthEdited: key === 'width' ? true : current.widthEdited
};
};
const updateRouteXsection = (route, xsection, manifest) => {
const technology = getTechnologyManifest(manifest);
const current = createRouteSettings(technology, route);
const xsectionInfo = getXsectionInfo(xsection, technology);
const next = {
...current,
xsection,
family: xsectionInfo.family || current.family
};
if (!current.widthEdited) {
next.width = Number(xsectionInfo.default_width ?? current.width);
}
next.radius = Number(xsectionInfo.default_radius ?? current.radius);
return next;
};
const routeStyleForSettings = (route, selected) => {
const settings = createRouteSettings(null, route);
const palette = {
strip: '#38bdf8',
rib_low: '#22c55e',
metal_1: '#f59e0b',
metal_2: '#f97316'
};
const electrical = settings.family === 'electrical';
const strokeWidth = electrical ? 3.5 : 2.4;
return {
type: electrical ? 'step' : 'smoothstep',
style: {
stroke: palette[settings.xsection] || palette.strip,
strokeWidth: selected ? strokeWidth + 1.2 : strokeWidth,
strokeDasharray: electrical ? '8 5' : undefined,
filter: selected ? 'drop-shadow(0 0 5px rgba(255,255,255,0.45))' : undefined
}
};
};
const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE;
const isBasicComponent = (componentName) => Boolean(BASIC_COMPONENTS[componentName]);
const createBasicSettings = (componentName, overrides) => ({
...(BASIC_COMPONENTS[componentName] ? BASIC_COMPONENTS[componentName].settings : {}),
...(overrides || {})
});
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 fallbackPercent = (index, count) => {
if (count <= 1) return 50;
return roundPercent(15 + (index / (count - 1)) * 70);
};
const positiveNumber = (value) => {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : null;
};
const normalizeBoxSize = (metadata, fallback) => {
const fallbackSize = fallback || DEFAULT_COMPONENT_BOX_SIZE;
const raw = metadata && (metadata.box_size || metadata.box_sz || metadata.boxSize);
let width = null;
let height = null;
if (Array.isArray(raw)) {
width = positiveNumber(raw[0]);
height = positiveNumber(raw[1]);
} else if (raw && typeof raw === 'object') {
width = positiveNumber(raw.width ?? raw.w ?? raw.x);
height = positiveNumber(raw.height ?? raw.h ?? raw.y);
}
return {
width: width || fallbackSize.width,
height: height || fallbackSize.height
};
};
const chooseCategoryComponent = (dragName, availableComponents, categoryName) => {
const available = Array.isArray(availableComponents)
? availableComponents.filter(Boolean)
: [];
if (dragName && !isForgeComponent(dragName)) return dragName;
const physicalComponent = available.find(component => !isForgeComponent(component));
return physicalComponent || dragName || available[0] || categoryName;
};
const normalizeCanvasSize = (size) => ({
width: positiveNumber(size && size.width) || DEFAULT_CANVAS_SIZE.width,
height: positiveNumber(size && size.height) || DEFAULT_CANVAS_SIZE.height
});
const clampPositionToCanvas = (position, canvasSize, boxSize) => {
const size = normalizeCanvasSize(canvasSize);
const box = normalizeBoxSize({ box_size: [boxSize && boxSize.width, boxSize && boxSize.height] });
const maxX = Math.max(0, size.width - box.width);
const maxY = Math.max(0, size.height - box.height);
return {
x: Math.min(maxX, Math.max(0, Number(position && position.x) || 0)),
y: Math.min(maxY, Math.max(0, Number(position && position.y) || 0))
};
};
const transformBoxCorner = (corner, transform) => {
const options = transform || {};
let x = Number(corner && corner.x) || 0;
let y = Number(corner && corner.y) || 0;
if (options.flop) x = -x;
if (options.flip) y = -y;
const rotation = Number(options.rotation || 0);
if (!rotation) return { x, y };
const radians = rotation * Math.PI / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
return {
x: x * cos - y * sin,
y: x * sin + y * cos
};
};
const roundBoundsValue = (value) => Number(value.toFixed(6));
const calculateLayoutBounds = (pageOrNodes) => {
const page = Array.isArray(pageOrNodes) ? { nodes: pageOrNodes } : (pageOrNodes || {});
const nodes = Array.isArray(page.nodes) ? page.nodes : [];
const points = [];
nodes.forEach(node => {
if (!node || !node.position || !node.data || !node.data.componentName || node.data.elementType) return;
const box = normalizeBoxSize({ box_size: node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const origin = {
x: Number(node.position.x) || 0,
y: Number(node.position.y) || 0
};
[
{ x: 0, y: 0 },
{ x: box.width, y: 0 },
{ x: box.width, y: box.height },
{ x: 0, y: box.height }
].forEach(corner => {
const transformed = transformBoxCorner(corner, node.data);
points.push({
x: origin.x + transformed.x,
y: origin.y + transformed.y
});
});
});
if (points.length === 0) {
const size = normalizeCanvasSize(page.canvasSize || DEFAULT_CANVAS_SIZE);
points.push({ x: 0, y: 0 }, { x: size.width, y: size.height });
}
const minX = roundBoundsValue(Math.min(...points.map(point => point.x)));
const maxX = roundBoundsValue(Math.max(...points.map(point => point.x)));
const minY = roundBoundsValue(Math.min(...points.map(point => point.y)));
const maxY = roundBoundsValue(Math.max(...points.map(point => point.y)));
return {
minX,
minY,
maxX,
maxY,
width: Math.max(1, maxX - minX),
height: Math.max(1, maxY - minY),
bottomLeft: { x: minX, y: minY },
topRight: { x: maxX, y: maxY }
};
};
const roundMeasureValue = (value) => Number(value.toFixed(3));
const normalizeMeasurePoint = (point) => {
const x = Number(point && point.x);
const y = Number(point && point.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
};
const createRulerMeasurement = (startPoint, endPoint) => {
const start = normalizeMeasurePoint(startPoint);
const end = normalizeMeasurePoint(endPoint);
if (!start || !end) return null;
const dx = roundMeasureValue(end.x - start.x);
const dy = roundMeasureValue(end.y - start.y);
const distance = roundMeasureValue(Math.hypot(dx, dy));
const midpoint = {
x: roundMeasureValue((start.x + end.x) / 2),
y: roundMeasureValue((start.y + end.y) / 2)
};
return {
start,
end,
dx,
dy,
distance,
midpoint,
label: `${distance.toFixed(3)} um dx ${dx.toFixed(3)} dy ${dy.toFixed(3)}`
};
};
const createComponentSymbolMetrics = (boxSize) => {
const size = normalizeBoxSize({ box_size: [boxSize && boxSize.width, boxSize && boxSize.height] });
const widthRatio = size.width >= 400 ? 0.95 : 0.9;
return {
width: roundMeasureValue(size.width * widthRatio),
height: roundMeasureValue(size.height * 0.68)
};
};
const transformPortInfo = (info, transform) => {
const source = info || {};
const options = transform || {};
let x = Number(source.x || 0);
let y = Number(source.y || 0);
let angle = Number(source.a || 0);
if (options.flip) {
y = -y;
angle = -angle;
}
if (options.flop) {
x = -x;
angle = 180 - angle;
}
const rotation = Number(options.rotation || 0);
if (rotation) {
const radians = rotation * Math.PI / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
const nextX = x * cos - y * sin;
const nextY = x * sin + y * cos;
x = nextX;
y = nextY;
angle += rotation;
}
return {
...source,
x,
y,
a: normalizeAngle(angle)
};
};
const buildSideHandles = (ports, side) => {
const vertical = side === 'left' || side === 'right';
return ports.map((port, index) => {
const percent = fallbackPercent(index, ports.length);
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, transform) => {
const grouped = { left: [], right: [], top: [], bottom: [] };
Object.entries(ports || {}).forEach(([name, info]) => {
if (name === 'a0' || name === 'b0') return;
const transformedInfo = transformPortInfo(info, transform);
const side = portSideFromAngle(transformedInfo.a);
grouped[side].push({ name, info: transformedInfo });
});
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 canvasToLayoutY = (value) => -Number(value || 0);
const layoutToCanvasY = (value) => -Number(value || 0);
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, flip, flop, forgeArguments, basicArguments }) => {
const forge = isForgeComponent(componentName);
const basic = isBasicComponent(componentName);
const componentValue = forge ? FORGE_COMPONENT_TYPE : (basic ? componentName : componentPath);
const settings = forge ? createForgeArguments(forgeArguments) : null;
const settingsYaml = forge
? `\n settings:\n${buildSettingsYaml(settings, 6)}`
: basic
? `\n settings:\n${buildSettingsYaml(createBasicSettings(componentName, basicArguments), 6)}`
: '\n settings:\n length:';
return ` ${instanceName}:
component: ${componentValue}
x: ${Number(position.x || 0).toFixed(1)}
y: ${canvasToLayoutY(position.y).toFixed(1)}
rotation: ${Number(rotation || 0).toFixed(1)}
flip: ${flip ? 1 : 0}
flop: ${flop ? 1 : 0}
mirror: ${flip ? 'true' : '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,
flip: Boolean(data.flip),
flop: Boolean(data.flop),
forgeArguments: data.forgeArguments,
basicArguments: data.basicArguments
});
})
.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 buildBasicComponentPorts = (componentName, settings) => {
const values = createBasicSettings(componentName, settings);
const length = Number(values.length || 0);
const radius = Number(values.radius || 10);
const width = Number(values.width ?? values.width1 ?? 0.5);
const xsection = values.xsection || values.xs || 'strip';
if (componentName === 'waveguide') {
return {
a1: { x: 0, y: 0, a: 180, width, xsection, description: 'Optical power input' },
b1: { x: length, y: 0, a: 0, width, xsection, description: 'Optical power output' }
};
}
if (componentName === '90 bend') {
return {
a1: { x: 0, y: 0, a: 180, width, xsection, description: 'Optical power input' },
b1: { x: radius, y: radius, a: 90, width, xsection, description: 'Optical power output' }
};
}
if (componentName === '180 bend') {
return {
a1: { x: 0, y: 0, a: 180, width, xsection, description: 'Optical power input' },
b1: { x: 0, y: 2 * radius, a: 180, width, xsection, description: 'Optical power output' }
};
}
if (componentName === 'cricle' || componentName === 'circle') {
return {
a1: { x: radius, y: 0, a: 180, width, xsection, description: 'Optical power input' },
b1: { x: radius, y: 0, a: 180, width, xsection, description: 'Optical power output' }
};
}
if (componentName === 'taper') {
return {
a1: { x: 0, y: 0, a: 180, width: Number(values.width1 || width), xsection, description: 'Optical power input' },
b1: { x: length, y: 0, a: 0, width: Number(values.width2 || width), xsection, description: 'Optical power output' }
};
}
return {};
};
const getBasicComponentMetadata = (componentName, settings) => {
if (!isBasicComponent(componentName)) return null;
const values = createBasicSettings(componentName, settings);
const length = Number(values.length || 0);
const radius = Number(values.radius || 10);
const width = Number(values.width ?? values.width1 ?? 0.5);
const width2 = Number(values.width2 ?? width);
const boxSize = componentName === 'waveguide'
? [Math.max(length, 10), Math.max(width * 4, 4)]
: componentName === 'taper'
? [Math.max(length, 10), Math.max(width, width2) * 10 + 18]
: componentName === '180 bend'
? [radius, radius * 2]
: [radius, radius];
return {
name: componentName,
foundry: 'mxpic',
process: 'basic nazca',
ports: buildBasicComponentPorts(componentName, values),
box_size: boxSize,
settings: values
};
};
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: ${canvasToLayoutY(info.y).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: ${canvasToLayoutY((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')}`;
};
const buildBundlesYaml = (page, manifest) => {
const { nodes = [], edges = [] } = page || {};
const nodeMap = {};
nodes.forEach(n => { nodeMap[n.id] = n; });
let linksYaml = '';
if (edges.length > 0) {
const linkLines = edges.map(edge => {
const sourceNode = nodeMap[edge.source];
const targetNode = nodeMap[edge.target];
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
const fromPort = edge.sourceHandle || 'unknown';
const toPort = edge.targetHandle || 'unknown';
const route = createRouteSettings(manifest, edge.data && edge.data.route);
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')}`
: '';
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
if (isFreeRoute) {
return ` - id: ${toYamlScalar(edge.id)}
xsection: ${route.xsection}
family: ${route.family}
width: ${Number(route.width)}
radius: ${Number(route.radius)}
routing_type: ${route.routing_type}${pointsYaml}`;
}
return ` - from: ${sourceName}:${fromPort}
to: ${targetName}:${toPort}
xsection: ${route.xsection}
family: ${route.family}
width: ${Number(route.width)}
radius: ${Number(route.radius)}
routing_type: ${route.routing_type}${pointsYaml}`;
});
linksYaml = linkLines.join('\n');
}
return `# 3. Bundles (Grouped links for multi-bus/parallel routing)
bundles:
output_bus:
routing_type: euler_bend
links:
${linksYaml}`;
};
const getNodeCenter = (node) => {
if (!node) return null;
return {
x: Number((node.position && node.position.x) || 0),
y: Number((node.position && node.position.y) || 0)
};
};
const getNodePortCanvasPoint = (node, portName) => {
if (!node) return null;
const x = Number((node.position && node.position.x) || 0);
const y = Number((node.position && node.position.y) || 0);
if (node.type === 'portNode' || (node.data && node.data.elementType === 'port')) {
return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
}
const ports = node.data && node.data.ports;
const portInfo = ports && portName ? ports[portName] : null;
if (!portInfo) return null;
const transformedInfo = transformPortInfo(portInfo, {
rotation: (node.data && node.data.rotation) || 0,
flip: Boolean(node.data && node.data.flip),
flop: Boolean(node.data && node.data.flop)
});
return {
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
};
};
const percentValue = (value, fallback = 50) => {
if (typeof value !== 'string') return fallback;
const number = Number(value.replace('%', ''));
return Number.isFinite(number) ? number : fallback;
};
const getEdgeEndpointPoint = (edge, nodeMap, endpoint) => {
const nodeId = endpoint === 'source' ? edge.source : edge.target;
const handleId = endpoint === 'source' ? edge.sourceHandle : edge.targetHandle;
const node = nodeMap[nodeId];
if (!node) return null;
const pinPoint = getNodePortCanvasPoint(node, handleId);
if (pinPoint) return pinPoint;
const ports = node.data && node.data.ports;
if (ports && handleId) {
const handles = buildPortHandles(ports, {
rotation: (node.data && node.data.rotation) || 0,
flip: Boolean(node.data && node.data.flip),
flop: Boolean(node.data && node.data.flop)
});
const handle = handles.find(item => item.name === handleId);
if (handle) {
const componentSize = normalizeBoxSize({ box_size: node.data && node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
let x = Number((node.position && node.position.x) || 0);
let y = Number((node.position && node.position.y) || 0);
if (handle.position === 'left') {
y += componentSize.height * percentValue(handle.style && handle.style.top) / 100;
} else if (handle.position === 'right') {
x += componentSize.width;
y += componentSize.height * percentValue(handle.style && handle.style.top) / 100;
} else if (handle.position === 'top') {
x += componentSize.width * percentValue(handle.style && handle.style.left) / 100;
} else {
x += componentSize.width * percentValue(handle.style && handle.style.left) / 100;
y += componentSize.height;
}
return { x: Number(x.toFixed(3)), y: Number(y.toFixed(3)) };
}
}
return null;
};
const getEdgeRoutePoints = (edge, nodeMap) => {
const explicitPoints = edge && edge.data && Array.isArray(edge.data.points) ? edge.data.points : [];
if (explicitPoints.length >= 2) {
const points = explicitPoints
.map(point => ({
x: Number(point && point.x),
y: Number(point && point.y)
}))
.filter(point => Number.isFinite(point.x) && Number.isFinite(point.y));
if (!Boolean(edge.data && edge.data.freeRoute) && points.length >= 2) {
const sourcePoint = getEdgeEndpointPoint(edge, nodeMap, 'source');
const targetPoint = getEdgeEndpointPoint(edge, nodeMap, 'target');
if (sourcePoint) points[0] = sourcePoint;
if (targetPoint) points[points.length - 1] = targetPoint;
}
return points;
}
return [getNodeCenter(nodeMap[edge.source]), getNodeCenter(nodeMap[edge.target])].filter(Boolean);
};
const routeSegmentsIntersect = (pointsA, pointsB) => {
for (let i = 0; i < pointsA.length - 1; i += 1) {
for (let j = 0; j < pointsB.length - 1; j += 1) {
if (segmentsIntersect(pointsA[i], pointsA[i + 1], pointsB[j], pointsB[j + 1])) {
return true;
}
}
}
return false;
};
const orientation = (a, b, c) => {
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
if (Math.abs(value) < 1e-9) return 0;
return value > 0 ? 1 : 2;
};
const segmentsIntersect = (p1, q1, p2, q2) => {
if (!p1 || !q1 || !p2 || !q2) return false;
const o1 = orientation(p1, q1, p2);
const o2 = orientation(p1, q1, q2);
const o3 = orientation(p2, q2, p1);
const o4 = orientation(p2, q2, q1);
return o1 !== o2 && o3 !== o4;
};
const findSameTypeRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
const candidateRoute = createRouteSettings(manifest, candidateEdge.data && candidateEdge.data.route);
const candidatePoints = getEdgeRoutePoints(candidateEdge, nodeMap);
for (const edge of existingEdges || []) {
if (!edge || edge.id === candidateEdge.id) continue;
if (edge.source === candidateEdge.source || edge.source === candidateEdge.target || edge.target === candidateEdge.source || edge.target === candidateEdge.target) continue;
const route = createRouteSettings(manifest, edge.data && edge.data.route);
if (route.xsection !== candidateRoute.xsection) continue;
const points = getEdgeRoutePoints(edge, nodeMap);
if (routeSegmentsIntersect(candidatePoints, points)) {
return { conflictEdge: edge, xsection: route.xsection };
}
}
return null;
};
const findSameFamilyRouteCrossing = findSameTypeRouteCrossing;
return {
FORGE_COMPONENT_LABEL,
FORGE_COMPONENT_TYPE,
DEFAULT_COMPONENT_BOX_SIZE,
DEFAULT_CANVAS_SIZE,
PORT_NODE_SIZE,
ELEMENT_COMPONENTS,
BASIC_COMPONENTS,
DEFAULT_FORGE_ARGUMENTS,
FALLBACK_TECHNOLOGY_MANIFEST,
canvasToLayoutY,
layoutToCanvasY,
createForgeArguments,
createRouteSettings,
updateRouteField,
updateRouteXsection,
routeStyleForSettings,
findSameTypeRouteCrossing,
findSameFamilyRouteCrossing,
isForgeComponent,
isBasicComponent,
createBasicSettings,
normalizeAngle,
portSideFromAngle,
normalizeBoxSize,
chooseCategoryComponent,
normalizeCanvasSize,
clampPositionToCanvas,
calculateLayoutBounds,
createRulerMeasurement,
createComponentSymbolMetrics,
transformPortInfo,
getNodePortCanvasPoint,
buildPortHandles,
buildElementPorts,
buildBasicComponentPorts,
getBasicComponentMetadata,
buildInstanceYaml,
buildInstancesYaml,
buildPageComponentPorts,
buildCanvasPortsYaml,
buildBundlesYaml,
buildPortsYaml,
buildElementsYaml,
buildSettingsYaml,
toYamlScalar
};
});