define([ './BaseComponent', 'svg/SVGUtilities', 'svg/SVGShapes', ], ( BaseComponent, svg, SVGShapes ) => { 'use strict'; function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) { container.appendChild(svg.make( attrs.fill === 'none' ? 'polyline' : 'polygon', Object.assign({ 'points': ( (x + dx) + ' ' + (y - dy) + ' ' + x + ' ' + y + ' ' + (x + dx) + ' ' + (y + dy) ), }, attrs) )); } class Arrowhead { constructor(propName) { this.propName = propName; } getConfig(theme) { return theme.connect.arrow[this.propName]; } short(theme) { const arrow = this.getConfig(theme); const join = arrow.attrs['stroke-linejoin'] || 'miter'; const t = arrow.attrs['stroke-width'] * 0.5; const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5; if(join === 'round') { return lineStroke + t; } else { const h = arrow.height / 2; const w = arrow.width; const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); return lineStroke + arrowDistance; } } render(layer, theme, {x, y, dir}) { const config = this.getConfig(theme); drawHorizontalArrowHead(layer, { x: x + this.short(theme) * dir, y, dx: config.width * dir, dy: config.height / 2, attrs: config.attrs, }); } width(theme) { return this.short(theme) + this.getConfig(theme).width; } height(theme) { return this.getConfig(theme).height; } lineGap(theme, lineAttrs) { const arrow = this.getConfig(theme); const short = this.short(theme); if(arrow.attrs.fill === 'none') { const h = arrow.height / 2; const w = arrow.width; const safe = short + (lineAttrs['stroke-width'] / 2) * (w / h); return (short + safe) / 2; } else { return short + arrow.width / 2; } } } const ARROWHEADS = [ { render: () => {}, width: () => 0, height: () => 0, lineGap: () => 0, }, new Arrowhead('single'), new Arrowhead('double'), ]; class Connect extends BaseComponent { separation({label, agentNames, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; let labelWidth = ( env.textSizer.measure(config.label.attrs, label).width ); if(labelWidth > 0) { labelWidth += config.label.padding * 2; } const info1 = env.agentInfos.get(agentNames[0]); if(agentNames[0] === agentNames[1]) { env.addSpacing(agentNames[0], { left: 0, right: ( info1.currentMaxRad + Math.max( labelWidth + lArrow.width(env.theme), rArrow.width(env.theme) ) + config.loopbackRadius ), }); } else { const info2 = env.agentInfos.get(agentNames[1]); env.addSeparation( agentNames[0], agentNames[1], info1.currentMaxRad + info2.currentMaxRad + labelWidth + Math.max( lArrow.width(env.theme), rArrow.width(env.theme) ) * 2 ); } } renderSelfConnect({label, agentNames, options}, env) { const config = env.theme.connect; const from = env.agentInfos.get(agentNames[0]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); const lineX = from.x + from.currentMaxRad; const y0 = env.primaryY; const x0 = ( lineX + lArrow.width(env.theme) + (label ? config.label.padding : 0) ); const renderedText = SVGShapes.renderBoxedText(label, { x: x0 - config.mask.padding.left, y: y0 - height + config.label.margin.top, padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.loopbackAttrs, boxLayer: env.maskLayer, labelLayer: env.labelLayer, SVGTextBlockClass: env.SVGTextBlockClass, }); const labelW = (label ? ( renderedText.width + config.label.padding - config.mask.padding.left - config.mask.padding.right ) : 0); const r = config.loopbackRadius; const x1 = Math.max(lineX + rArrow.width(env.theme), x0 + labelW); const y1 = y0 + r * 2; const lineAttrs = config.lineAttrs[options.line]; env.shapeLayer.appendChild(svg.make('path', Object.assign({ 'd': ( 'M ' + (lineX + lArrow.lineGap(env.theme, lineAttrs)) + ' ' + y0 + ' L ' + x1 + ' ' + y0 + ' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + ' L ' + (lineX + rArrow.lineGap(env.theme, lineAttrs)) + ' ' + y1 ), }, lineAttrs))); lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1}); rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1}); return y1 + rArrow.height(env.theme) / 2 + env.theme.actionMargin; } renderSimpleConnect({label, agentNames, options}, env) { const config = env.theme.connect; const from = env.agentInfos.get(agentNames[0]); const to = env.agentInfos.get(agentNames[1]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const dir = (from.x < to.x) ? 1 : -1; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); const x0 = from.x + from.currentMaxRad * dir; const x1 = to.x - to.currentMaxRad * dir; let y = env.primaryY; SVGShapes.renderBoxedText(label, { x: (x0 + x1) / 2, y: y - height + config.label.margin.top, padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.attrs, boxLayer: env.maskLayer, labelLayer: env.labelLayer, SVGTextBlockClass: env.SVGTextBlockClass, }); const lineAttrs = config.lineAttrs[options.line]; env.shapeLayer.appendChild(svg.make('line', Object.assign({ 'x1': x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, 'y1': y, 'x2': x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, 'y2': y, }, lineAttrs))); lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir}); rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir}); return ( y + Math.max( lArrow.height(env.theme), rArrow.height(env.theme) ) / 2 + env.theme.actionMargin ); } renderPre({label, agentNames, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); let arrowH = lArrow.height(env.theme); if(agentNames[0] !== agentNames[1]) { arrowH = Math.max(arrowH, rArrow.height(env.theme)); } return { agentNames, topShift: Math.max(arrowH / 2, height), }; } render(stage, env) { if(stage.agentNames[0] === stage.agentNames[1]) { return this.renderSelfConnect(stage, env); } else { return this.renderSimpleConnect(stage, env); } } } BaseComponent.register('connect', new Connect()); return Connect; });