Files
mxpic_EDA/frontend/canvas-helpers.js
T

296 lines
10 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 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
};
});