import BaseComponent, {register} from './BaseComponent.mjs'; const OUTLINE_ATTRS = { 'class': 'outline', 'fill': 'transparent', }; function findExtremes(agentInfos, agentIDs) { let min = null; let max = null; agentIDs.forEach((id) => { const info = agentInfos.get(id); if(min === null || info.index < min.index) { min = info; } if(max === null || info.index > max.index) { max = info; } }); return { left: min.id, right: max.id, }; } function findEdges(fullW, { x0 = null, x1 = null, xMid = null, }) { let xL = x0; let xR = x1; if(xL === null && xMid !== null) { xL = xMid - fullW / 2; } if(xR === null && xL !== null) { xR = xL + fullW; } else if(xL === null) { xL = xR - fullW; } return {xL, xR}; } function textAnchorX(anchor, {xL, xR}, padding) { switch(anchor) { case 'middle': return (xL + padding.left + xR - padding.right) / 2; case 'end': return xR - padding.right; default: return xL + padding.left; } } class NoteComponent extends BaseComponent { prepareMeasurements({mode, label}, env) { const config = env.theme.getNote(mode); env.textSizer.expectMeasure(config.labelAttrs, label); } renderPre({agentIDs}) { return {agentIDs}; } renderNote({ label, mode, position, }, env) { const config = env.theme.getNote(mode); const {padding} = config; const y = env.topY + config.margin.top + padding.top; const labelNode = env.svg.formattedText(config.labelAttrs, label); const size = env.textSizer.measure(labelNode); const fullW = (size.width + padding.left + padding.right); const fullH = (size.height + padding.top + padding.bottom); const edges = findEdges(fullW, position); labelNode.set({ x: textAnchorX(config.labelAttrs['text-anchor'], edges, padding), y, }); const boundingBox = { height: fullH, width: edges.xR - edges.xL, x: edges.xL, y: env.topY + config.margin.top, }; env.makeRegion().add( config.boxRenderer(boundingBox), env.svg.box(OUTLINE_ATTRS, boundingBox), labelNode ); return ( env.topY + config.margin.top + fullH + config.margin.bottom + env.theme.actionMargin ); } } export class NoteOver extends NoteComponent { separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const width = ( env.textSizer.measure(config.labelAttrs, label).width + config.padding.left + config.padding.right ); const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); if(infoL === infoR) { env.addSpacing(left, { left: width / 2, right: width / 2, }); } else { const hangL = infoL.currentMaxRad + config.overlap.left; const hangR = infoR.currentMaxRad + config.overlap.right; env.addSeparation(left, right, width - hangL - hangR); env.addSpacing(left, {left: hangL, right: 0}); env.addSpacing(right, {left: 0, right: hangR}); } } render({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); if(infoL === infoR) { const xMid = infoL.x; return this.renderNote({ label, mode, position: {xMid}, }, env); } else { return this.renderNote({ label, mode, position: { x0: infoL.x - infoL.currentMaxRad - config.overlap.left, x1: infoR.x + infoR.currentMaxRad + config.overlap.right, }, }, env); } } } export class NoteSide extends NoteComponent { constructor(isRight) { super(); this.isRight = isRight; } separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const {left, right} = findExtremes(env.agentInfos, agentIDs); const width = ( env.textSizer.measure(config.labelAttrs, label).width + config.padding.left + config.padding.right + config.margin.left + config.margin.right ); if(this.isRight) { const info = env.agentInfos.get(right); env.addSpacing(right, { left: 0, right: width + info.currentMaxRad, }); } else { const info = env.agentInfos.get(left); env.addSpacing(left, { left: width + info.currentMaxRad, right: 0, }); } } render({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const {left, right} = findExtremes(env.agentInfos, agentIDs); if(this.isRight) { const info = env.agentInfos.get(right); const x0 = info.x + info.currentMaxRad + config.margin.left; return this.renderNote({ label, mode, position: {x0}, }, env); } else { const info = env.agentInfos.get(left); const x1 = info.x - info.currentMaxRad - config.margin.right; return this.renderNote({ label, mode, position: {x1}, }, env); } } } export class NoteBetween extends NoteComponent { separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); env.addSeparation( left, right, env.textSizer.measure(config.labelAttrs, label).width + config.padding.left + config.padding.right + config.margin.left + config.margin.right + infoL.currentMaxRad + infoR.currentMaxRad ); } render({agentIDs, mode, label}, env) { const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); const xMid = ( infoL.x + infoL.currentMaxRad + infoR.x - infoR.currentMaxRad ) / 2; return this.renderNote({ label, mode, position: {xMid}, }, env); } } register('note over', new NoteOver()); register('note left', new NoteSide(false)); register('note right', new NoteSide(true)); register('note between', new NoteBetween());