From 6830d4038512bbdeab40f3010a174a6dc3220209 Mon Sep 17 00:00:00 2001 From: Klemek Date: Fri, 12 Jul 2019 16:40:41 +0200 Subject: [PATCH] nodes and links rendering (+debug) --- localtests.js | 112 +++++++++++++++++++++++++++++++++ src/rendering.js | 139 ++++++++++++++++++++++++++++++++++++----- test/rendering.test.js | 38 ++++++++--- 3 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 localtests.js diff --git a/localtests.js b/localtests.js new file mode 100644 index 0000000..cadf613 --- /dev/null +++ b/localtests.js @@ -0,0 +1,112 @@ +const faDiagrams = require('./src/index'); +const fs = require('fs'); + +/*const data = { + options: { + font: 'Courier New' + }, + nodes: [ + { + name: 'node1', + icon: 'server', + bottom: {text: 'myserver' }, + top: {icon: 'node'} + }, + { + name: 'node2', + icon: 'globe', + bottom: {text: 'world'} + } + ], + links: [ + { + type: 'arrow', + from: 'node1', + to: 'node2', + direction: 'right', + bottom: {text: 'Hello World!'} + } + ] +};*/ + +const data = { + options: { + rendering: { + beautify: true + }, + placing: { + diagonals: true + } + }, + nodes: [ + { + name: '1', + icon: 'circle', + }, + { + name: '2', + icon: 'circle' + }, + { + name: '3', + icon: 'circle' + }, + { + name: '4', + icon: 'circle' + }, + { + name: '5', + icon: 'circle' + }, + { + name: '6', + icon: 'circle' + }, + ], + links: [ + {from: '1', to: '2', direction: 'right', type: ''}, + {from: '1', to: '3', direction: 'down', type: ''}, + {from: '3', to: '4', direction: 'right', type: ''}, + {from: '4', to: '5', direction: 'up', type: 'double'}, + {from: '3', to: '6', direction: 'left', type: ''} + ] +}; + +fs.writeFileSync('out.svg', faDiagrams.compute(data), {encoding: 'utf-8'}); + +const rendering = require('./src/rendering')({ + 'beautify': false, + 'scale': 1, + 'h-spacing': 1, + 'icons': { + 'scale': 0.1 + }, + 'links': { + 'scale': 1 + }, +}); + +const g = []; + +let y = 0; + +for (let i = 0; i < 20; i++) { + if (i % 5 === 0) + y = 0; + ['', 'double', 'line'].forEach(type => { + g.push({ + '_attributes': { + 'transform': `translate(${Math.pow(Math.floor(i / 5), 1.6) * 720} ${256 * (y++)})` + }, + 'path': { + '_attributes': { + 'd': rendering.getLinkPath(type, (i + 1) / 5) + } + } + }); + }); + +} + +fs.writeFileSync('out2.svg', rendering.toXML({g: g}, {w: 7000, h: 256 * 16}), {encoding: 'utf-8'}); diff --git a/src/rendering.js b/src/rendering.js index 39a1be1..e08d5f9 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -15,10 +15,27 @@ try { * @property {string} icon */ +/** + * @typedef Link2 + * @property {string} from + * @property {string} to + * @property {string|undefined} type + */ + const DEFAULT_OPTIONS = { - 'beautify': false + 'beautify': false, + 'scale': 128, + 'h-spacing': 1.3, + 'icons': { + 'scale': 1 + }, + 'links': { + 'scale': 1 + }, }; +const DEFAULT_SCALE = 0.4; + module.exports = (options = DEFAULT_OPTIONS) => { const self = { defaultOptions: DEFAULT_OPTIONS, @@ -61,31 +78,118 @@ module.exports = (options = DEFAULT_OPTIONS) => { return null; }, + getLinkPath: (type, width) => { + switch (type) { + case 'line': + return `M12 216c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h${width * 488}c6.627 0 12 -5.373 12 -12v-56c0 -6.627 -5.373 -12 -12 -12z`; + case 'double': + const scale = 363.88; + return `M${134.059 + width * scale} 216h-${width * scale}v-46.059c0-21.382-25.851-32.09-40.971-16.971l-86.059 86.059c-9.373 9.373-9.373 24.568 0 33.941l86.059 86.059c15.119 15.119 40.971 4.411 40.971-16.971v-46.059h${width * scale}v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.568 0-33.941l-86.059-86.059c-15.119-15.12-40.971-4.412-40.971 16.97z`; + default: + return `M12 216c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h${width * 425}v46.059c0 21.382 25.851 32.09 40.971 16.971 l86.059 -86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z`; + } + }, + /** + * Get the width and height of the graph of nodes * @param {Object} nodes - * @returns {Object} + * @returns {{w: number, h: number}} */ - renderNodes: (nodes) => { - const g = []; - Object.values(nodes).forEach(() => { - //TODO + getBounds: (nodes) => { + const list = Object.values(nodes); + if (list.length === 0) + return {w: 0, h: 0}; //empty + let maxX = 0; + let maxY = 0; + list.forEach(n => { + maxX = Math.max(n.x, maxX); + maxY = Math.max(n.y, maxY); + }); + return {w: maxX + 1, h: maxY + 1}; + }, + + /** + * @param {{g:Object[]}} data + * @param {Object} nodes + */ + renderNodes: (data, nodes) => { + Object.values(nodes).forEach(node => { + const icon = self.getIcon(node.icon); + if (icon) { + const scale = (node['scale'] || options['icons']['scale']) * DEFAULT_SCALE; + const group = { + '_attributes': { + 'transform': `translate(${(node.x + 0.5) * options['h-spacing']} ${node.y + 0.5})` + }, + 'g': { + '_attributes': { + 'transform': `scale(${scale / 512} ${scale / 512}) translate(${-icon.width / 2} ${-256})` + }, + 'path': { + '_attributes': { + 'd': icon.path, + } + } + } + }; + data['g'].push(group); + } + }); + }, + + /** + * @param {{g:Object[]}} data + * @param {Object} nodes + * @param {Link2[]} links + */ + renderLinks: (data, nodes, links) => { + links.forEach(link => { + const src = nodes[link.from]; + const dst = nodes[link.to]; + + const posX = ((src.x + dst.x) / 2 + 0.5) * options['h-spacing']; + const posY = (src.y + dst.y) / 2 + 0.5; + + const angle = Math.atan2(dst.y - src.y, (dst.x - src.x) * options['h-spacing']) * 180 / Math.PI; + + const size = Math.sqrt(Math.pow((dst.x - src.x) * options['h-spacing'], 2) + Math.pow(dst.y - src.y, 2)); + + const path = self.getLinkPath(link.type, link['size'] || size); + + const scale = (link['scale'] || options['links']['scale']) * DEFAULT_SCALE; + const group = { + '_attributes': { + 'transform': `translate(${posX} ${posY}) rotate(${angle})` + }, + 'g': { + '_attributes': { + 'transform': `scale(${scale / 512} ${scale / 512}) translate(${(-256 * size)} ${-256})` + }, + 'path': { + '_attributes': { + 'd': path + } + } + } + }; + data['g'].push(group); }); - return {'g': g}; }, /** * Convert xml-js data into correct svg xml string * @param {Object} data - * @param {number} width - * @param {number} height + * @param {{w:number, h:number}} bounds * @returns {string} */ - toXML: (data, width, height) => { + toXML: (data, bounds) => { const xml = { 'svg': { '_attributes': { 'xmlns': 'http://www.w3.org/2000/svg', - 'viewBox': `0 0 ${width} ${height}` + 'viewBox': `0 0 ${bounds.w * options['h-spacing']} ${bounds.h}`, + 'width': bounds.w * options['h-spacing'] * options['scale'], + 'height': bounds.h * options['scale'], } } }; @@ -98,9 +202,16 @@ module.exports = (options = DEFAULT_OPTIONS) => { }); }, - compute: (nodes) => { - const data = self.renderNodes(nodes); - return self.toXML(data, 0, 0); //TODO temporary + compute: (nodes, links) => { + + const bounds = self.getBounds(nodes); + + const data = {'g': []}; + + self.renderNodes(data, nodes); + self.renderLinks(data, nodes, links); + + return self.toXML(data, bounds); } }; diff --git a/test/rendering.test.js b/test/rendering.test.js index 6751f03..f428751 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -1,10 +1,10 @@ /* jshint -W117 */ const rendering = require('../src/rendering'); -describe('getIcon', () => { - const solidCirclePath = 'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z'; - const regularCirclePath = 'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z'; +const solidCirclePath = 'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z'; +const regularCirclePath = 'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z'; +describe('getIcon', () => { test('no name', () => { const res = rendering().getIcon(undefined); expect(res).toBeNull(); @@ -81,14 +81,32 @@ describe('getIcon', () => { }); }); +describe('getBounds', () => { + test('no nodes', () => { + const res = rendering({beautify: false}).getBounds({}); + expect(res).toEqual({w: 0, h: 0}); + }); + test('1 node', () => { + const res = rendering({beautify: false}).getBounds({ + '1': {x: 0, y: 0} + }); + expect(res).toEqual({w: 1, h: 1}); + }); + test('3 nodes', () => { + const res = rendering({beautify: false}).getBounds({ + '1': {x: 0, y: 0}, '2': {x: 5, y: 2}, '3': {x: 3, y: 8}, + }); + expect(res).toEqual({w: 6, h: 9}); + }); +}); describe('toXML', () => { test('no data', () => { - const res = rendering({beautify: false}).toXML({}, 0, 0); - expect(res).toEqual(''); + const res = rendering({beautify: false, scale: 20, 'h-spacing': 1}).toXML({}, {w: 0, h: 0}); + expect(res).toEqual(''); }); test('sample svg data', () => { - const res = rendering({beautify: false}).toXML({ + const res = rendering({beautify: false, scale: 2, 'h-spacing': 1}).toXML({ 'circle': { '_attributes': { 'cx': 50, @@ -96,14 +114,14 @@ describe('toXML', () => { 'r': 50 } } - }, 100, 100); - expect(res).toEqual(''); + }, {w: 100, h: 100}); + expect(res).toEqual(''); }); }); describe('compute', () => { test('no nodes no links', () => { - const res = rendering({beautify: true}).compute({}, []); - expect(res).toEqual('\n'); + const res = rendering({beautify: true, 'h-spacing': 1.2, scale: 20}).compute({}, []); + expect(res).toEqual('\n'); }); }); \ No newline at end of file