Routing nchor added
This commit is contained in:
+463
-35
@@ -7,6 +7,9 @@
|
||||
})(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',
|
||||
@@ -19,11 +22,44 @@
|
||||
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 }
|
||||
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',
|
||||
@@ -137,6 +173,12 @@
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -157,28 +199,206 @@
|
||||
|
||||
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 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';
|
||||
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 percent = fallbackPercent(index, ports.length);
|
||||
const percentValue = `${percent}%`;
|
||||
const style = vertical
|
||||
? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' }
|
||||
@@ -193,12 +413,13 @@
|
||||
});
|
||||
};
|
||||
|
||||
const buildPortHandles = (ports) => {
|
||||
const buildPortHandles = (ports, transform) => {
|
||||
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 || {} });
|
||||
const transformedInfo = transformPortInfo(info, transform);
|
||||
const side = portSideFromAngle(transformedInfo.a);
|
||||
grouped[side].push({ name, info: transformedInfo });
|
||||
});
|
||||
|
||||
Object.values(grouped).forEach(sidePorts => {
|
||||
@@ -229,6 +450,9 @@
|
||||
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 || {});
|
||||
@@ -236,18 +460,25 @@
|
||||
return entries.map(([key, value]) => `${pad}${key}: ${toYamlScalar(value)}`).join('\n');
|
||||
};
|
||||
|
||||
const buildInstanceYaml = ({ instanceName, componentName, componentPath, position, rotation, forgeArguments }) => {
|
||||
const buildInstanceYaml = ({ instanceName, componentName, componentPath, position, rotation, flip, flop, forgeArguments, basicArguments }) => {
|
||||
const forge = isForgeComponent(componentName);
|
||||
const componentValue = forge ? FORGE_COMPONENT_TYPE : componentPath;
|
||||
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)}` : '\n settings:\n length:';
|
||||
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: ${Number(position.y || 0).toFixed(1)}
|
||||
y: ${canvasToLayoutY(position.y).toFixed(1)}
|
||||
rotation: ${Number(rotation || 0).toFixed(1)}
|
||||
mirror: false${settingsYaml}`;
|
||||
flip: ${flip ? 1 : 0}
|
||||
flop: ${flop ? 1 : 0}
|
||||
mirror: ${flip ? 'true' : 'false'}${settingsYaml}`;
|
||||
};
|
||||
|
||||
const buildInstancesYaml = ({ nodes, resolveComponentPath }) => {
|
||||
@@ -266,7 +497,10 @@
|
||||
componentPath,
|
||||
position: node.position || { x: 0, y: 0 },
|
||||
rotation: data.rotation || 0,
|
||||
forgeArguments: data.forgeArguments
|
||||
flip: Boolean(data.flip),
|
||||
flop: Boolean(data.flop),
|
||||
forgeArguments: data.forgeArguments,
|
||||
basicArguments: data.basicArguments
|
||||
});
|
||||
})
|
||||
.join('\n\n');
|
||||
@@ -296,6 +530,69 @@
|
||||
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) {
|
||||
@@ -332,7 +629,7 @@
|
||||
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)}
|
||||
y: ${canvasToLayoutY(info.y).toFixed(1)}
|
||||
angle: ${Number(info.a || 0).toFixed(1)}
|
||||
width: ${Number(info.width || 0.5)}${description}`;
|
||||
});
|
||||
@@ -351,7 +648,7 @@
|
||||
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)}
|
||||
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)}
|
||||
@@ -375,13 +672,27 @@
|
||||
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}`;
|
||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||
});
|
||||
linksYaml = linkLines.join('\n');
|
||||
}
|
||||
@@ -402,6 +713,103 @@ ${linksYaml}`;
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -417,41 +825,61 @@ ${linksYaml}`;
|
||||
return o1 !== o2 && o3 !== o4;
|
||||
};
|
||||
|
||||
const findSameFamilyRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
|
||||
const findSameTypeRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
|
||||
const candidateRoute = createRouteSettings(manifest, candidateEdge.data && candidateEdge.data.route);
|
||||
const candidateStart = getNodeCenter(nodeMap[candidateEdge.source]);
|
||||
const candidateEnd = getNodeCenter(nodeMap[candidateEdge.target]);
|
||||
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.family !== candidateRoute.family) continue;
|
||||
const start = getNodeCenter(nodeMap[edge.source]);
|
||||
const end = getNodeCenter(nodeMap[edge.target]);
|
||||
if (segmentsIntersect(candidateStart, candidateEnd, start, end)) {
|
||||
return { conflictEdge: edge, family: route.family };
|
||||
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,
|
||||
|
||||
+1857
-332
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user