/**
* JointJS cell factories: pipeline nodes (SVG-symbol elements with ports)
* and pipeline links. Shapes are registered under the `pipeline` namespace
* so graph.fromJSON can rebuild them.
*/
import { dia, shapes } from '@joint/core';
import type { NodeTypeDef } from '../model/nodeTypes';
import { getNodeType, defaultNodeProps } from '../model/nodeTypes';
import type { PropertyValue } from '../model/properties';
import type { PortSpec } from '../model/validation';
import { getColorScheme } from './colorSchemes';
import { genId } from '../model/graph';
export const PipelineNode = dia.Element.define(
'pipeline.Node',
{
attrs: {
root: { magnet: false },
image: { x: 0, y: 0 },
label: {
textAnchor: 'middle',
fontSize: 12,
fontFamily: 'sans-serif',
fill: '#334155',
},
},
},
{
markup: [
{ tagName: 'image', selector: 'image' },
{ tagName: 'text', selector: 'label' },
],
},
);
export const PipelineLink = shapes.standard.Link.define('pipeline.Link', {
attrs: {
line: { stroke: '#2563eb', strokeWidth: 2 },
wrapper: { strokeWidth: 12 },
},
});
/** Namespace for dia.Graph cell rebuilding. */
export const cellNamespace = {
...shapes,
pipeline: { Node: PipelineNode, Link: PipelineLink },
};
/** Render a node type's SVG symbol to a data URI with a concrete color. */
export function svgDataUri(type: NodeTypeDef, color: string): string {
const svg =
``;
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
export function portAbsolutePosition(type: NodeTypeDef, port: PortSpec): { x: number; y: number } {
return { x: port.x * type.width, y: port.y * type.height };
}
/** Create a JointJS element for a node type at the given position. */
export function createNode(
typeId: string,
x: number,
y: number,
schemeId: string,
props?: Record,
id?: string,
): dia.Element {
const type = getNodeType(typeId);
if (!type) throw new Error(`Unknown node type: ${typeId}`);
const scheme = getColorScheme(schemeId);
const bag = { ...defaultNodeProps(typeId), ...props, x, y };
const element = new PipelineNode({
id: id ?? genId('node'),
position: { x, y },
size: { width: type.width, height: type.height },
nodeTypeId: typeId,
props: bag,
ports: {
groups: {
default: {
position: 'absolute',
attrs: {
portBody: {
r: 7,
magnet: true,
stroke: '#ffffff',
strokeWidth: 1.5,
cursor: 'crosshair',
class: 'pf-port-body',
},
},
markup: [{ tagName: 'circle', selector: 'portBody' }],
label: {
position: { name: 'radial', args: { offset: 12 } },
markup: [{ tagName: 'text', selector: 'text', className: 'pf-port-label' }],
},
},
},
items: type.ports.map((port) => ({
id: port.id,
group: 'default',
args: portAbsolutePosition(type, port),
attrs: {
portBody: { fill: scheme.relations[port.relation] },
text: { text: port.label ?? port.id, fontSize: 10, fill: scheme.nodeStroke },
},
})),
},
});
applyNodePresentation(element, schemeId);
return element;
}
/** Sync presentation-group props (position, angle, color, label) onto the cell. */
export function applyNodePresentation(element: dia.Element, schemeId: string): void {
const typeId = element.get('nodeTypeId') as string;
const type = getNodeType(typeId);
if (!type) return;
const scheme = getColorScheme(schemeId);
const props = (element.get('props') ?? {}) as Record;
const color = typeof props.symbolColor === 'string' && props.symbolColor ? props.symbolColor : scheme.nodeStroke;
const title = typeof props.title === 'string' ? props.title : type.label;
const labelVisible = props.labelVisible !== 'no';
element.attr({
image: {
href: svgDataUri(type, color),
width: type.width,
height: type.height,
},
label: {
text: labelVisible ? title : '',
x: type.width / 2,
y: type.height + 14,
fill: scheme.nodeStroke,
},
});
// Refresh port colors for the active scheme.
for (const port of type.ports) {
element.portProp(port.id, 'attrs/portBody/fill', scheme.relations[port.relation]);
element.portProp(port.id, 'attrs/text/fill', scheme.nodeStroke);
}
const angle = Number(props.angle ?? 0);
if (element.angle() !== angle) {
element.rotate(angle - element.angle());
}
}
/** Look up the model PortSpec behind a JointJS element port. */
export function portSpec(element: dia.Cell, portId: string): PortSpec | undefined {
const typeId = element.get('nodeTypeId') as string | undefined;
if (!typeId) return undefined;
return getNodeType(typeId)?.ports.find((p) => p.id === portId);
}