More annotation added to the program

This commit is contained in:
2026-05-30 12:44:44 +08:00
parent b3f29398f0
commit bf223b52ac
22 changed files with 729 additions and 353 deletions
+74
View File
@@ -5,19 +5,28 @@
* Organization : OptiHK Limited
*/
(function (root, factory) {
// Build the helper API once, then expose it to both browser and Node test environments.
const helpers = factory();
if (typeof module === 'object' && module.exports) {
module.exports = helpers;
}
root.MxpicCanvasHelpers = helpers;
})(typeof window !== 'undefined' ? window : globalThis, function () {
// Label used by the canvas to represent generated mxpic_forge components.
const FORGE_COMPONENT_LABEL = 'generate with mxpic_forge';
// Serialized component type used when saving mxpic_forge-generated components.
const FORGE_COMPONENT_TYPE = 'generate_with_forge';
// Fallback visual size for PDK components without explicit metadata.
const DEFAULT_COMPONENT_BOX_SIZE = { width: 132, height: 82 };
// Default editable canvas dimensions in micrometers.
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
// Base visual diameter and hit area used for port and anchor handles.
const PORT_NODE_SIZE = 30;
// Narrow anchor body width used in the canvas visual representation.
const ANCHOR_NODE_WIDTH = 8;
// Default spacing between repeated anchor or port pins.
const DEFAULT_ELEMENT_PITCH = 10;
// Defines built-in port and anchor element metadata before per-node expansion.
const ELEMENT_COMPONENTS = {
Port: {
name: 'Port',
@@ -35,6 +44,7 @@
}
}
};
// Defines local primitive components that do not require PDK lookup.
const BASIC_COMPONENTS = {
waveguide: {
name: 'waveguide',
@@ -69,6 +79,7 @@
}
};
// Default parameters sent when creating a component through mxpic_forge.
const DEFAULT_FORGE_ARGUMENTS = {
function_name: 'straight',
component_name: '',
@@ -87,6 +98,7 @@
notes: ''
};
// Fallback routing technology data used when the backend manifest is unavailable.
const FALLBACK_TECHNOLOGY_MANIFEST = {
routing_types: ['euler_bend', 'standard_bend'],
defaults: {
@@ -104,18 +116,22 @@
}
};
// Merge user edits with default mxpic_forge arguments for saving and generation.
const createForgeArguments = (overrides) => ({
...DEFAULT_FORGE_ARGUMENTS,
...(overrides || {})
});
// Return a manifest object, falling back to bundled defaults when needed.
const getTechnologyManifest = (manifest) => manifest || FALLBACK_TECHNOLOGY_MANIFEST;
// Look up width, radius, and family defaults for a routing cross-section.
const getXsectionInfo = (xsection, manifest) => {
const technology = getTechnologyManifest(manifest);
return (technology.xsections && technology.xsections[xsection]) || technology.xsections.strip || {};
};
// Normalize route settings so every edge has xsection, family, width, radius, and bend type.
const createRouteSettings = (manifest, overrides) => {
const technology = getTechnologyManifest(manifest);
const defaults = technology.defaults || FALLBACK_TECHNOLOGY_MANIFEST.defaults;
@@ -132,6 +148,7 @@
};
};
// Apply a single route-field edit while preserving route defaults and width override state.
const updateRouteField = (route, key, value, manifest) => {
const current = createRouteSettings(manifest, route);
const numericFields = new Set(['width', 'radius']);
@@ -143,6 +160,7 @@
};
};
// Switch an edge cross-section and refresh dependent routing defaults.
const updateRouteXsection = (route, xsection, manifest) => {
const technology = getTechnologyManifest(manifest);
const current = createRouteSettings(technology, route);
@@ -159,6 +177,7 @@
return next;
};
// Convert route settings into React Flow edge styling for canvas display.
const routeStyleForSettings = (route, selected) => {
const settings = createRouteSettings(null, route);
const palette = {
@@ -180,14 +199,18 @@
};
};
// Check whether a component name refers to the mxpic_forge generator placeholder.
const isForgeComponent = (componentName) => componentName === FORGE_COMPONENT_LABEL || componentName === FORGE_COMPONENT_TYPE;
// Check whether a component is one of the built-in primitive canvas elements.
const isBasicComponent = (componentName) => Boolean(BASIC_COMPONENTS[componentName]);
// Merge primitive component defaults with user-entered values.
const createBasicSettings = (componentName, overrides) => ({
...(BASIC_COMPONENTS[componentName] ? BASIC_COMPONENTS[componentName].settings : {}),
...(overrides || {})
});
// Normalize angles into the -180 to 180 degree range used by port logic.
const normalizeAngle = (angle) => {
const value = Number(angle);
if (!Number.isFinite(value)) return 0;
@@ -196,6 +219,7 @@
return Object.is(normalized, -0) ? 0 : normalized;
};
// Map a port angle to the canvas side where its handle should appear.
const portSideFromAngle = (angle) => {
const normalized = normalizeAngle(angle);
if (normalized === 0) return 'right';
@@ -205,18 +229,22 @@
return Math.abs(normalized) < 90 ? 'right' : 'left';
};
// Round handle percentages so saved and rendered positions stay stable.
const roundPercent = (value) => Number(value.toFixed(3));
// Generate even fallback spacing for handles when no exact position is available.
const fallbackPercent = (index, count) => {
if (count <= 1) return 50;
return roundPercent(15 + (index / (count - 1)) * 70);
};
// Accept only finite positive numeric values from metadata or user input.
const positiveNumber = (value) => {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : null;
};
// Resolve component visual dimensions from metadata with a safe fallback.
const normalizeBoxSize = (metadata, fallback) => {
const fallbackSize = fallback || DEFAULT_COMPONENT_BOX_SIZE;
const raw = metadata && (metadata.box_size || metadata.box_sz || metadata.boxSize);
@@ -235,6 +263,7 @@
};
};
// Select the physical component to place when a library category is dragged.
const chooseCategoryComponent = (dragName, availableComponents, categoryName) => {
const available = Array.isArray(availableComponents)
? availableComponents.filter(Boolean)
@@ -244,11 +273,13 @@
return physicalComponent || dragName || available[0] || categoryName;
};
// Resolve valid canvas dimensions from saved project metadata.
const normalizeCanvasSize = (size) => ({
width: positiveNumber(size && size.width) || DEFAULT_CANVAS_SIZE.width,
height: positiveNumber(size && size.height) || DEFAULT_CANVAS_SIZE.height
});
// Keep dragged or pasted nodes inside the active canvas bounds.
const clampPositionToCanvas = (position, canvasSize, boxSize) => {
const size = normalizeCanvasSize(canvasSize);
const box = normalizeBoxSize({ box_size: [boxSize && boxSize.width, boxSize && boxSize.height] });
@@ -260,6 +291,7 @@
};
};
// Rotate or mirror a component-box corner for layout bounds calculation.
const transformBoxCorner = (corner, transform) => {
const options = transform || {};
let x = Number(corner && corner.x) || 0;
@@ -277,8 +309,10 @@
};
};
// Round layout-bound values for stable preview and export metadata.
const roundBoundsValue = (value) => Number(value.toFixed(6));
// Calculate the bounding rectangle containing all visible canvas nodes.
const calculateLayoutBounds = (pageOrNodes) => {
const page = Array.isArray(pageOrNodes) ? { nodes: pageOrNodes } : (pageOrNodes || {});
const nodes = Array.isArray(page.nodes) ? page.nodes : [];
@@ -326,8 +360,10 @@
};
};
// Round ruler measurements for compact display.
const roundMeasureValue = (value) => Number(value.toFixed(3));
// Convert pointer coordinates into valid ruler measurement points.
const normalizeMeasurePoint = (point) => {
const x = Number(point && point.x);
const y = Number(point && point.y);
@@ -335,6 +371,7 @@
return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
};
// Build distance, delta, and midpoint values for the ruler overlay.
const createRulerMeasurement = (startPoint, endPoint) => {
const start = normalizeMeasurePoint(startPoint);
const end = normalizeMeasurePoint(endPoint);
@@ -357,6 +394,7 @@
};
};
// Derive compact symbol dimensions for drawing component previews.
const createComponentSymbolMetrics = (boxSize) => {
const size = normalizeBoxSize({ box_size: [boxSize && boxSize.width, boxSize && boxSize.height] });
const widthRatio = size.width >= 400 ? 0.95 : 0.9;
@@ -366,6 +404,7 @@
};
};
// Apply node rotation and mirror transforms to a component port definition.
const transformPortInfo = (info, transform) => {
const source = info || {};
const options = transform || {};
@@ -402,6 +441,7 @@
};
};
// Create ordered React Flow handles for all ports on a single visual side.
const buildSideHandles = (ports, side) => {
const vertical = side === 'left' || side === 'right';
@@ -421,6 +461,7 @@
});
};
// Group transformed ports into canvas handles with side and position styling.
const buildPortHandles = (ports, transform) => {
const grouped = { left: [], right: [], top: [], bottom: [] };
Object.entries(ports || {}).forEach(([name, info]) => {
@@ -447,6 +488,7 @@
];
};
// Serialize primitive JavaScript values into YAML-friendly scalar text.
const toYamlScalar = (value) => {
if (value === null || value === undefined) return '""';
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
@@ -458,9 +500,12 @@
return JSON.stringify(String(value));
};
// Convert canvas Y coordinates into layout Y coordinates.
const canvasToLayoutY = (value) => -Number(value || 0);
// Convert layout Y coordinates back into canvas Y coordinates.
const layoutToCanvasY = (value) => -Number(value || 0);
// Serialize nested component settings into YAML blocks.
const buildSettingsYaml = (settings, indent) => {
const pad = ' '.repeat(indent);
const entries = Object.entries(settings || {});
@@ -468,6 +513,7 @@
return entries.map(([key, value]) => `${pad}${key}: ${toYamlScalar(value)}`).join('\n');
};
// Serialize one component instance into saved layout YAML.
const buildInstanceYaml = ({ instanceName, componentName, componentPath, position, rotation, flip, flop, forgeArguments, basicArguments }) => {
const forge = isForgeComponent(componentName);
const basic = isBasicComponent(componentName);
@@ -489,6 +535,7 @@
mirror: ${flip ? 'true' : 'false'}${settingsYaml}`;
};
// Serialize all component nodes on a page into the instances YAML section.
const buildInstancesYaml = ({ nodes, resolveComponentPath }) => {
return (nodes || [])
.filter(node => node.data && node.data.componentName && !node.data.elementType)
@@ -514,26 +561,33 @@
.join('\n\n');
};
// Resolve the display/export name for a port-like node.
const getNodePortName = (node) => {
const name = node && node.data && (node.data.portName || node.data.componentDisplayName || node.data.label);
return name || (node && node.id) || 'port';
};
// Detect standalone port nodes that become top-level layout ports.
const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode');
// Detect built-in port or anchor nodes for element YAML export.
const isElementNode = (node) => node && node.data && (node.data.elementType === 'port' || node.data.elementType === 'anchor');
// Clamp repeated port counts to a supported positive integer.
const normalizePortNumber = (value) => {
const number = Math.floor(Number(value));
return Number.isFinite(number) ? Math.max(1, number) : 1;
};
// Resolve repeated port spacing with the default pitch fallback.
const normalizePitch = (value) => {
const number = Number(value);
return Number.isFinite(number) ? Math.max(0, number) : DEFAULT_ELEMENT_PITCH;
};
// Calculate the centered offset for one repeated port index.
const elementPortOffset = (index, count, pitch) => ((count - 1) / 2 - index) * pitch;
// Grow port and anchor visual bodies so repeated port circles do not overlap.
const buildElementBoxSize = (data) => {
const portNumber = normalizePortNumber(data && data.portNumber);
const pitch = normalizePitch(data && data.pitch);
@@ -544,6 +598,7 @@
};
};
// Expand port and anchor definitions into one or more named physical ports.
const buildElementPorts = (elementType, data) => {
const element = ELEMENT_COMPONENTS[elementType === 'anchor' ? 'Anchor' : 'Port'];
if (!element) return {};
@@ -583,6 +638,7 @@
return JSON.parse(JSON.stringify(element.ports));
};
// Generate port metadata for built-in primitive components.
const buildBasicComponentPorts = (componentName, settings) => {
const values = createBasicSettings(componentName, settings);
const length = Number(values.length || 0);
@@ -622,6 +678,7 @@
return {};
};
// Generate visual metadata and port definitions for primitive components.
const getBasicComponentMetadata = (componentName, settings) => {
if (!isBasicComponent(componentName)) return null;
const values = createBasicSettings(componentName, settings);
@@ -646,6 +703,7 @@
};
};
// Convert standalone port nodes into page-level layout ports.
const buildPageComponentPorts = (port, nodes) => {
const portNodes = (nodes || []).filter(isPortElementNode);
if (portNodes.length > 0) {
@@ -683,6 +741,7 @@
};
};
// Serialize standalone canvas ports into a layout ports YAML section.
const buildCanvasPortsYaml = (nodes, fallbackPort) => {
const ports = buildPageComponentPorts(fallbackPort, nodes);
const entries = Object.entries(ports);
@@ -701,8 +760,10 @@
return `ports:\n${lines.join('\n')}`;
};
// Maintain legacy single-port YAML export behavior for older callers.
const buildPortsYaml = (port) => buildCanvasPortsYaml([], port);
// Serialize built-in port and anchor nodes into layout element metadata.
const buildElementsYaml = (nodes) => {
const elementNodes = (nodes || []).filter(isElementNode);
if (elementNodes.length === 0) return 'elements: {}';
@@ -726,6 +787,7 @@
return `elements:\n${lines.join('\n')}`;
};
// Serialize canvas links into routed bundle YAML including route settings and bend points.
const buildBundlesYaml = (page, manifest) => {
const { nodes = [], edges = [] } = page || {};
const nodeMap = {};
@@ -774,6 +836,7 @@ bundles:
${linksYaml}`;
};
// Return the center point of a node when a more precise port point is unavailable.
const getNodeCenter = (node) => {
if (!node) return null;
return {
@@ -782,6 +845,7 @@ ${linksYaml}`;
};
};
// Resolve the exact canvas coordinate of a named node port or anchor pin.
const getNodePortCanvasPoint = (node, portName) => {
if (!node) return null;
const x = Number((node.position && node.position.x) || 0);
@@ -824,12 +888,14 @@ ${linksYaml}`;
};
};
// Convert handle percent strings into numeric ratios for endpoint lookup.
const percentValue = (value, fallback = 50) => {
if (typeof value !== 'string') return fallback;
const number = Number(value.replace('%', ''));
return Number.isFinite(number) ? number : fallback;
};
// Resolve a route endpoint from an edge handle and its connected node.
const getEdgeEndpointPoint = (edge, nodeMap, endpoint) => {
const nodeId = endpoint === 'source' ? edge.source : edge.target;
const handleId = endpoint === 'source' ? edge.sourceHandle : edge.targetHandle;
@@ -871,6 +937,7 @@ ${linksYaml}`;
return null;
};
// Return the editable route polyline points for crossing checks and YAML export.
const getEdgeRoutePoints = (edge, nodeMap) => {
const explicitPoints = edge && edge.data && Array.isArray(edge.data.points) ? edge.data.points : [];
if (explicitPoints.length >= 2) {
@@ -891,6 +958,7 @@ ${linksYaml}`;
return [getNodeCenter(nodeMap[edge.source]), getNodeCenter(nodeMap[edge.target])].filter(Boolean);
};
// Check two routed polylines for any segment crossing.
const routeSegmentsIntersect = (pointsA, pointsB) => {
for (let i = 0; i < pointsA.length - 1; i += 1) {
for (let j = 0; j < pointsB.length - 1; j += 1) {
@@ -902,12 +970,14 @@ ${linksYaml}`;
return false;
};
// Classify point ordering for the line-segment intersection test.
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;
};
// Detect whether two line segments cross each other.
const segmentsIntersect = (p1, q1, p2, q2) => {
if (!p1 || !q1 || !p2 || !q2) return false;
const o1 = orientation(p1, q1, p2);
@@ -917,6 +987,7 @@ ${linksYaml}`;
return o1 !== o2 && o3 !== o4;
};
// Normalize equivalent route xsection names before same-type crossing checks.
const routeTypeKey = (route) => {
const xsection = String((route && route.xsection) || '').trim().toLowerCase();
if (xsection === 'metal1') return 'metal_1';
@@ -925,6 +996,7 @@ ${linksYaml}`;
return xsection;
};
// Find an existing same-type route that would cross a candidate edge.
const findSameTypeRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
const candidateRoute = createRouteSettings(manifest, candidateEdge.data && candidateEdge.data.route);
const candidateType = routeTypeKey(candidateRoute);
@@ -942,8 +1014,10 @@ ${linksYaml}`;
return null;
};
// Backward-compatible alias for same-type route crossing validation.
const findSameFamilyRouteCrossing = findSameTypeRouteCrossing;
// Expose the helper functions consumed by canvas.html and the Node-based tests.
return {
FORGE_COMPONENT_LABEL,
FORGE_COMPONENT_TYPE,