import { Anchor, BOUNDS, Component, Connectable, LinkEnd, RectPath, Shape } from '@hatiolab/things-scene' export enum DIRECTION { NORTH = 'N', EAST = 'E', SOUTH = 'S', WEST = 'W' } export enum INOUT { IN = 'in', OUT = 'out', INOUT = 'inout' } export enum PIPELINE { MESSAGE = 'message', CONTROL = 'control', EVENT = 'event', DOCUMENT = 'document' } export type NODE = { name: string description: string inout: INOUT type: PIPELINE direction: DIRECTION multiplicity?: number } const STYLE_MAP = { message: { strokeStyle: 'navy', fillStyle: 'navy' }, control: { strokeStyle: 'red', fillStyle: 'red', lineDash: 'long-dash-dot' }, event: { strokeStyle: 'yellow', fillStyle: 'yellow', lineDash: 'dash' }, document: { strokeStyle: 'black', fillStyle: 'black', lineDash: 'round-dot' } } type FILTER_FUNCTION = (this: Anchor, counterEnd?: LinkEnd) => boolean const SIDE_SHORT = 2 const SIDE_LONG = 6 const TEMPLATE = { type: 'pipeline', lineWidth: 2, strokeStyle: 'black', begin: 'oval', beginSize: 'size9', end: 'arrow', endSize: 'size9', round: 10 } function drawAnchor(this: Anchor, context: CanvasRenderingContext2D, component: Component) { const bounds = this.bounds if (!bounds) { return } const { left, top, width, height } = bounds const styleMap = STYLE_MAP[this.type || 'fluid'] || STYLE_MAP['fluid'] context.strokeStyle = styleMap.strokeStyle context.strokeStyle = styleMap.fillStyle context.rect(left, top, width, height) context.fill() context.stroke() } function linkFilter(this: Anchor, counterEnd?: LinkEnd) { if (!counterEnd) { return this.inout !== 'in' } const { component, anchor } = counterEnd || {} /* FIXME remove !anchor.type condition */ return !anchor.type || anchor.type === this.type } export default abstract class ControlBase extends Connectable(RectPath(Shape)) { abstract getImageElement(): HTMLImageElement | undefined get nodes(): NODE[] { return [] } render(ctx: CanvasRenderingContext2D) { var { left, top, width, height } = this.bounds ctx.translate(left, top) ctx.beginPath() var radius = 9 ctx.moveTo(radius, 0) ctx.arcTo(width, 0, width, height, radius) ctx.arcTo(width, height, 0, height, radius) ctx.arcTo(0, height, 0, 0, radius) ctx.arcTo(0, 0, width, 0, radius) this.drawFill(ctx) ctx.beginPath() const image = this.getImageElement() if (image) { this.drawImage(ctx, image, 0, 0, width, height) } ctx.translate(-left, -top) } get controls() { return [] } get anchors(): Array { var allNodes = this.nodes var { left, top, width, height } = this.bounds var right = left + width var bottom = top + height const dirs: Array> = [DIRECTION.NORTH, DIRECTION.EAST, DIRECTION.SOUTH, DIRECTION.WEST].map( direction => { const nodes = allNodes.filter(node => node.direction === direction) if (nodes.length === 0) { return [] } var dx = 0 var dy = 0 var sx = 0 var sy = 0 var bounds: BOUNDS switch (direction) { case DIRECTION.NORTH: bounds = { left: left - SIDE_SHORT / 2, top: top - SIDE_LONG, width: SIDE_SHORT, height: SIDE_LONG } sx = left sy = top - SIDE_LONG dx = width / (nodes.length + 1) dy = 0 break case DIRECTION.EAST: bounds = { left: right, top: top - SIDE_SHORT / 2, width: SIDE_LONG, height: SIDE_SHORT } sx = right + SIDE_LONG sy = top dx = 0 dy = height / (nodes.length + 1) break case DIRECTION.SOUTH: bounds = { left: left - SIDE_SHORT / 2, top: bottom, width: SIDE_SHORT, height: SIDE_LONG } sx = left sy = bottom + SIDE_LONG dx = width / (nodes.length + 1) dy = 0 break case DIRECTION.WEST: bounds = { left: left - SIDE_LONG, top: top - SIDE_SHORT / 2, width: SIDE_LONG, height: SIDE_SHORT } sx = left - SIDE_LONG sy = top dx = 0 dy = height / (nodes.length + 1) break default: } return nodes.map((node, idx) => { return { type: node.type as string, name: node.name, inout: node.inout, position: { x: sx + dx * (idx + 1), y: sy + dy * (idx + 1) }, bounds: { left: bounds.left + dx * (idx + 1), top: bounds.top + dy * (idx + 1), width: bounds.width, height: bounds.height }, draw: drawAnchor, filter: linkFilter, template: { ...TEMPLATE, ...(STYLE_MAP[node.type || 'message'] || STYLE_MAP['message']) }, multiplicity: node.multiplicity } }) } ) return dirs.flat() } get textBounds(): BOUNDS { var { paddingTop = 0, paddingLeft = 0, paddingRight = 0, paddingBottom = 0 } = this.model var { left, top, width, height } = this.bounds return { left: left + paddingLeft, top: top + paddingTop + height + 10, width: Math.max(width - paddingLeft - paddingRight, 0), height: 0 } } }