mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-09 17:19:45 +02:00
feat(arch): improved positioning system to better handle edge cases
This commit is contained in:
@@ -5,9 +5,11 @@ import type {
|
|||||||
ArchitectureGroup,
|
ArchitectureGroup,
|
||||||
ArchitectureDirection,
|
ArchitectureDirection,
|
||||||
ArchitectureLine,
|
ArchitectureLine,
|
||||||
|
ArchitectureDirectionPairMap,
|
||||||
|
ArchitectureDirectionPair,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
import { isArchitectureDirection } from './architectureTypes.js';
|
import { getArchitectureDirectionPair, isArchitectureDirection, shiftPositionByArchitectureDirectionPair } from './architectureTypes.js';
|
||||||
import {
|
import {
|
||||||
setAccTitle,
|
setAccTitle,
|
||||||
getAccTitle,
|
getAccTitle,
|
||||||
@@ -24,7 +26,7 @@ import type { D3Element } from '../../mermaidAPI.js';
|
|||||||
export const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
export const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||||
DEFAULT_CONFIG.architecture;
|
DEFAULT_CONFIG.architecture;
|
||||||
export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = {
|
export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = {
|
||||||
services: [],
|
services: {},
|
||||||
groups: [],
|
groups: [],
|
||||||
lines: [],
|
lines: [],
|
||||||
registeredIds: {},
|
registeredIds: {},
|
||||||
@@ -35,18 +37,20 @@ let services = DEFAULT_ARCHITECTURE_DB.services;
|
|||||||
let groups = DEFAULT_ARCHITECTURE_DB.groups;
|
let groups = DEFAULT_ARCHITECTURE_DB.groups;
|
||||||
let lines = DEFAULT_ARCHITECTURE_DB.lines;
|
let lines = DEFAULT_ARCHITECTURE_DB.lines;
|
||||||
let registeredIds = DEFAULT_ARCHITECTURE_DB.registeredIds;
|
let registeredIds = DEFAULT_ARCHITECTURE_DB.registeredIds;
|
||||||
|
let datastructures = DEFAULT_ARCHITECTURE_DB.datastructures;
|
||||||
let elements: Record<string, D3Element> = {};
|
let elements: Record<string, D3Element> = {};
|
||||||
|
|
||||||
const clear = (): void => {
|
const clear = (): void => {
|
||||||
services = structuredClone(DEFAULT_ARCHITECTURE_DB.services);
|
services = structuredClone(DEFAULT_ARCHITECTURE_DB.services);
|
||||||
groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups);
|
groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups);
|
||||||
lines = structuredClone(DEFAULT_ARCHITECTURE_DB.lines);
|
lines = structuredClone(DEFAULT_ARCHITECTURE_DB.lines);
|
||||||
registeredIds = structuredClone(DEFAULT_ARCHITECTURE_DB.registeredIds)
|
registeredIds = structuredClone(DEFAULT_ARCHITECTURE_DB.registeredIds);
|
||||||
|
datastructures = undefined;
|
||||||
elements = {};
|
elements = {};
|
||||||
commonClear();
|
commonClear();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addService = function (id: string, opts: Omit<ArchitectureService, 'id'> = {}) {
|
const addService = function (id: string, opts: Omit<ArchitectureService, 'id' | 'edges'> = {}) {
|
||||||
const { icon, in: inside, title } = opts;
|
const { icon, in: inside, title } = opts;
|
||||||
if (registeredIds[id] !== undefined) {
|
if (registeredIds[id] !== undefined) {
|
||||||
throw new Error(`The service id [${id}] is already in use by another ${registeredIds[id]}`)
|
throw new Error(`The service id [${id}] is already in use by another ${registeredIds[id]}`)
|
||||||
@@ -65,14 +69,16 @@ const addService = function (id: string, opts: Omit<ArchitectureService, 'id'> =
|
|||||||
|
|
||||||
registeredIds[id] = 'service';
|
registeredIds[id] = 'service';
|
||||||
|
|
||||||
services.push({
|
services[id] = {
|
||||||
id,
|
id,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
|
edges: [],
|
||||||
in: inside,
|
in: inside,
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
const getServices = (): ArchitectureService[] => services;
|
|
||||||
|
const getServices = (): ArchitectureService[] => Object.values(services);
|
||||||
|
|
||||||
const addGroup = function (id: string, opts: Omit<ArchitectureGroup, 'id'> = {}) {
|
const addGroup = function (id: string, opts: Omit<ArchitectureGroup, 'id'> = {}) {
|
||||||
const { icon, in: inside, title } = opts;
|
const { icon, in: inside, title } = opts;
|
||||||
@@ -100,7 +106,63 @@ const addGroup = function (id: string, opts: Omit<ArchitectureGroup, 'id'> = {})
|
|||||||
in: inside,
|
in: inside,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const getGroups = (): ArchitectureGroup[] => groups;
|
const getGroups = (): ArchitectureGroup[] => {
|
||||||
|
return groups
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getDataStructures = () => {
|
||||||
|
console.log('===== createSpatialMap =====')
|
||||||
|
if (datastructures === undefined) {
|
||||||
|
// Create an adjacency list of the diagram to perform BFS on
|
||||||
|
// Outer reduce applied on all services
|
||||||
|
// Inner reduce applied on the edges for a service
|
||||||
|
const adjList = Object.entries(services).reduce<{[id: string]: ArchitectureDirectionPairMap}>((prev, [id, service]) => {
|
||||||
|
prev[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prev, edge) => {
|
||||||
|
if (edge.lhs_id === id) { // source is LHS
|
||||||
|
const pair = getArchitectureDirectionPair(edge.lhs_dir, edge.rhs_dir);
|
||||||
|
if (pair) {
|
||||||
|
prev[pair] = edge.rhs_id
|
||||||
|
}
|
||||||
|
} else { // source is RHS
|
||||||
|
const pair = getArchitectureDirectionPair(edge.rhs_dir, edge.lhs_dir);
|
||||||
|
if (pair) {
|
||||||
|
prev[pair] = edge.lhs_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, {})
|
||||||
|
return prev
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const [firstId, _] = Object.entries(adjList)[0];
|
||||||
|
const spatialMap = {[firstId]: [0,0]};
|
||||||
|
const visited = {[firstId]: 1};
|
||||||
|
const queue = [firstId];
|
||||||
|
// Perform BFS on adjacency list
|
||||||
|
while(queue.length > 0) {
|
||||||
|
const id = queue.shift();
|
||||||
|
if (id) {
|
||||||
|
visited[id] = 1
|
||||||
|
const adj = adjList[id];
|
||||||
|
const [posX, posY] = spatialMap[id];
|
||||||
|
Object.entries(adj).forEach(([dir, rhsId]) => {
|
||||||
|
if (!visited[rhsId]) {
|
||||||
|
console.log(`${id} -- ${rhsId}`);
|
||||||
|
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair([posX, posY], dir as ArchitectureDirectionPair)
|
||||||
|
queue.push(rhsId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
datastructures = {
|
||||||
|
adjList,
|
||||||
|
spatialMap
|
||||||
|
}
|
||||||
|
console.log(datastructures)
|
||||||
|
}
|
||||||
|
return datastructures;
|
||||||
|
}
|
||||||
|
|
||||||
const addLine = function (
|
const addLine = function (
|
||||||
lhs_id: string,
|
lhs_id: string,
|
||||||
@@ -110,7 +172,6 @@ const addLine = function (
|
|||||||
opts: Omit<ArchitectureLine, 'lhs_id' | 'lhs_dir' | 'rhs_id' | 'rhs_dir'> = {}
|
opts: Omit<ArchitectureLine, 'lhs_id' | 'lhs_dir' | 'rhs_id' | 'rhs_dir'> = {}
|
||||||
) {
|
) {
|
||||||
const { title, lhs_into, rhs_into } = opts;
|
const { title, lhs_into, rhs_into } = opts;
|
||||||
|
|
||||||
if (!isArchitectureDirection(lhs_dir)) {
|
if (!isArchitectureDirection(lhs_dir)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid direction given for left hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${lhs_dir}`
|
`Invalid direction given for left hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${lhs_dir}`
|
||||||
@@ -121,8 +182,18 @@ const addLine = function (
|
|||||||
`Invalid direction given for right hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${rhs_dir}`
|
`Invalid direction given for right hand side of line ${lhs_id}--${rhs_id}. Expected (L,R,T,B) got ${rhs_dir}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (services[lhs_id] === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`The left-hand service [${lhs_id}] does not yet exist. Please create the service before declaring an edge to it.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (services[rhs_id] === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`The right-hand service [${rhs_id}] does not yet exist. Please create the service before declaring an edge to it.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
lines.push({
|
const edge = {
|
||||||
lhs_id,
|
lhs_id,
|
||||||
lhs_dir,
|
lhs_dir,
|
||||||
rhs_id,
|
rhs_id,
|
||||||
@@ -130,7 +201,12 @@ const addLine = function (
|
|||||||
title,
|
title,
|
||||||
lhs_into,
|
lhs_into,
|
||||||
rhs_into,
|
rhs_into,
|
||||||
});
|
}
|
||||||
|
|
||||||
|
lines.push(edge);
|
||||||
|
|
||||||
|
services[lhs_id].edges.push(lines[lines.length - 1])
|
||||||
|
services[rhs_id].edges.push(lines[lines.length - 1])
|
||||||
};
|
};
|
||||||
const getLines = (): ArchitectureLine[] => lines;
|
const getLines = (): ArchitectureLine[] => lines;
|
||||||
|
|
||||||
@@ -156,6 +232,7 @@ export const db: ArchitectureDB = {
|
|||||||
getLines,
|
getLines,
|
||||||
setElementForId,
|
setElementForId,
|
||||||
getElementById,
|
getElementById,
|
||||||
|
getDataStructures,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getConfigField<T extends keyof ArchitectureDiagramConfig>(field: T): Required<ArchitectureDiagramConfig>[T] {
|
function getConfigField<T extends keyof ArchitectureDiagramConfig>(field: T): Required<ArchitectureDiagramConfig>[T] {
|
||||||
|
@@ -14,12 +14,18 @@ import {
|
|||||||
type ArchitectureLine,
|
type ArchitectureLine,
|
||||||
type ArchitectureService,
|
type ArchitectureService,
|
||||||
isArchitectureDirectionY,
|
isArchitectureDirectionY,
|
||||||
|
ArchitectureDataStructures,
|
||||||
|
ArchitectureDirectionPair,
|
||||||
|
isArchitectureDirectionXY,
|
||||||
|
ArchitectureDirectionName,
|
||||||
|
getOppositeArchitectureDirection,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||||
import type { D3Element } from '../../mermaidAPI.js';
|
import type { D3Element } from '../../mermaidAPI.js';
|
||||||
import { drawEdges, drawGroups, drawService } from './svgDraw.js';
|
import { drawEdges, drawGroups, drawService } from './svgDraw.js';
|
||||||
import { getConfigField } from './architectureDb.js';
|
import { getConfigField } from './architectureDb.js';
|
||||||
|
import { X } from 'vitest/dist/reporters-5f784f42.js';
|
||||||
|
|
||||||
cytoscape.use(fcose);
|
cytoscape.use(fcose);
|
||||||
|
|
||||||
@@ -97,7 +103,8 @@ function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) {
|
|||||||
function layoutArchitecture(
|
function layoutArchitecture(
|
||||||
services: ArchitectureService[],
|
services: ArchitectureService[],
|
||||||
groups: ArchitectureGroup[],
|
groups: ArchitectureGroup[],
|
||||||
lines: ArchitectureLine[]
|
lines: ArchitectureLine[],
|
||||||
|
{adjList, spatialMap}: ArchitectureDataStructures
|
||||||
): Promise<cytoscape.Core> {
|
): Promise<cytoscape.Core> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||||
@@ -151,72 +158,76 @@ function layoutArchitecture(
|
|||||||
addServices(services, cy);
|
addServices(services, cy);
|
||||||
addEdges(lines, cy);
|
addEdges(lines, cy);
|
||||||
|
|
||||||
/**
|
// Use the spatial map to create alignment arrays for fcose
|
||||||
* Merge alignment pairs together if they share a common node.
|
const [horizontalAlignments, verticalAlignments] = (() => {
|
||||||
*
|
const _horizontalAlignments: Record<number, string[]> = {}
|
||||||
* Example: [["a", "b"], ["b", "c"], ["d", "e"]] -> [["a", "b", "c"], ["d", "e"]]
|
const _verticalAlignments: Record<number, string[]> = {}
|
||||||
*/
|
// Group service ids in an object with their x and y coordinate as the key
|
||||||
const mergeAlignments = (orig: string[][]): string[][] => {
|
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
||||||
if (orig.length < 1) return orig;
|
if (!_horizontalAlignments[y]) _horizontalAlignments[y] = [];
|
||||||
console.log('===== mergeAlignments =====');
|
if (!_verticalAlignments[x]) _verticalAlignments[x] = [];
|
||||||
console.log('Start: ', orig);
|
_horizontalAlignments[y].push(id);
|
||||||
// Mapping of discovered ids to their index in the new alignment array
|
_verticalAlignments[x].push(id);
|
||||||
const map: Record<string, number> = {};
|
})
|
||||||
const newAlignments: string[][] = [orig[0]];
|
|
||||||
map[orig[0][0]] = 0;
|
// Merge the values of each object into a list if the inner list has at least 2 elements
|
||||||
map[orig[0][1]] = 0;
|
return [
|
||||||
orig = orig.slice(1);
|
Object.values(_horizontalAlignments).filter(arr => arr.length > 1),
|
||||||
while (orig.length > 0) {
|
Object.values(_verticalAlignments).filter(arr => arr.length > 1)
|
||||||
const pair = orig[0];
|
]
|
||||||
const pairLHSIdx = map[pair[0]];
|
})();
|
||||||
const pairRHSIdx = map[pair[1]];
|
|
||||||
console.log(pair);
|
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||||
console.log(map);
|
const relativeConstraints = (() => {
|
||||||
console.log(newAlignments);
|
const _relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []
|
||||||
// If neither id appears in the new array, add the pair to the new array
|
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`
|
||||||
if (pairLHSIdx === undefined && pairRHSIdx === undefined) {
|
const strToPos = (pos: string) => pos.split(',').map(p => parseInt(p));
|
||||||
newAlignments.push(pair);
|
const invSpatialMap = Object.fromEntries(Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]))
|
||||||
map[pair[0]] = newAlignments.length - 1;
|
console.log('===== invSpatialMap =====')
|
||||||
map[pair[1]] = newAlignments.length - 1;
|
console.log(invSpatialMap);
|
||||||
// If the LHS of the pair doesn't appear in the new array, add the LHS to the existing array it shares an id with
|
|
||||||
} else if (pairLHSIdx === undefined) {
|
// perform BFS
|
||||||
newAlignments[pairRHSIdx].push(pair[0]);
|
const queue = [posToStr([0,0])];
|
||||||
map[pair[0]] = pairRHSIdx;
|
const visited: Record<string, number> = {};
|
||||||
// If the RHS of the pair doesn't appear in the new array, add the RHS to the existing array it shares an id with
|
const directions: Record<ArchitectureDirection, number[]> = {
|
||||||
} else if (pairRHSIdx === undefined) {
|
"L": [-1, 0],
|
||||||
newAlignments[pairLHSIdx].push(pair[1]);
|
"R": [1, 0],
|
||||||
map[pair[1]] = pairLHSIdx;
|
"T": [0, 1],
|
||||||
// If both ids already have been added to the new array and their index is different, merge all 3 arrays
|
"B": [0, -1]
|
||||||
} else if (pairLHSIdx != pairRHSIdx) {
|
|
||||||
console.log('ELSE');
|
|
||||||
newAlignments.push(pair);
|
|
||||||
}
|
}
|
||||||
orig = orig.slice(1);
|
while (queue.length > 0) {
|
||||||
|
const curr = queue.shift();
|
||||||
|
if (curr) {
|
||||||
|
visited[curr] = 1
|
||||||
|
const currId = invSpatialMap[curr];
|
||||||
|
if (currId) {
|
||||||
|
const currPos = strToPos(curr);
|
||||||
|
Object.entries(directions).forEach(([dir, shift]) => {
|
||||||
|
const newPos = posToStr([(currPos[0]+shift[0]), (currPos[1]+shift[1])]);
|
||||||
|
const newId = invSpatialMap[newPos];
|
||||||
|
// If there is an adjacent service to the current one and it has not yet been visited
|
||||||
|
if (newId && !visited[newPos]) {
|
||||||
|
queue.push(newPos);
|
||||||
|
// @ts-ignore cannot determine if left/right or top/bottom are paired together
|
||||||
|
_relativeConstraints.push({
|
||||||
|
[ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,
|
||||||
|
[ArchitectureDirectionName[getOppositeArchitectureDirection(dir as ArchitectureDirection)]]: currId,
|
||||||
|
gap: 100
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
console.log('End: ', newAlignments);
|
}
|
||||||
console.log('===========================');
|
}
|
||||||
|
}
|
||||||
return newAlignments;
|
return _relativeConstraints;
|
||||||
};
|
})();
|
||||||
|
console.log(`Horizontal Alignments:`)
|
||||||
const horizontalAlignments = cy
|
console.log(horizontalAlignments);
|
||||||
.edges()
|
console.log(`Vertical Alignments:`)
|
||||||
.filter(
|
console.log(verticalAlignments);
|
||||||
(edge) =>
|
console.log(`Relative Alignments:`)
|
||||||
isArchitectureDirectionX(edge.data('sourceDir')) &&
|
console.log(relativeConstraints);
|
||||||
isArchitectureDirectionX(edge.data('targetDir'))
|
|
||||||
)
|
|
||||||
.map((edge) => [edge.data('source'), edge.data('target')]);
|
|
||||||
|
|
||||||
const verticalAlignments = cy
|
|
||||||
.edges()
|
|
||||||
.filter(
|
|
||||||
(edge) =>
|
|
||||||
isArchitectureDirectionY(edge.data('sourceDir')) &&
|
|
||||||
isArchitectureDirectionY(edge.data('targetDir'))
|
|
||||||
)
|
|
||||||
.map((edge) => [edge.data('source'), edge.data('target')]);
|
|
||||||
|
|
||||||
cy.layout({
|
cy.layout({
|
||||||
name: 'fcose',
|
name: 'fcose',
|
||||||
@@ -242,34 +253,10 @@ function layoutArchitecture(
|
|||||||
return elasticity
|
return elasticity
|
||||||
},
|
},
|
||||||
alignmentConstraint: {
|
alignmentConstraint: {
|
||||||
horizontal: mergeAlignments(horizontalAlignments),
|
horizontal: horizontalAlignments,
|
||||||
vertical: mergeAlignments(verticalAlignments),
|
vertical: verticalAlignments,
|
||||||
},
|
},
|
||||||
relativePlacementConstraint: cy.edges().map((edge) => {
|
relativePlacementConstraint: relativeConstraints
|
||||||
const sourceDir = edge.data('sourceDir') as ArchitectureDirection;
|
|
||||||
const targetDir = edge.data('targetDir') as ArchitectureDirection;
|
|
||||||
const sourceId = edge.data('source') as string;
|
|
||||||
const targetId = edge.data('target') as string;
|
|
||||||
|
|
||||||
let gap = 1.25*getConfigField('iconSize');
|
|
||||||
console.log(`relativeConstraint: ${sourceId} ${sourceDir}--${targetDir} ${targetId} (gap=${gap})`);
|
|
||||||
if (isArchitectureDirectionX(sourceDir) && isArchitectureDirectionX(targetDir)) {
|
|
||||||
return {
|
|
||||||
left: sourceDir === 'R' ? sourceId : targetId,
|
|
||||||
right: sourceDir === 'L' ? sourceId : targetId,
|
|
||||||
gap,
|
|
||||||
};
|
|
||||||
} else if (isArchitectureDirectionY(sourceDir) && isArchitectureDirectionY(targetDir)) {
|
|
||||||
return {
|
|
||||||
top: sourceDir === 'B' ? sourceId : targetId,
|
|
||||||
bottom: sourceDir === 'T' ? sourceId : targetId,
|
|
||||||
gap,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('FALLBACK CASE NEEDED')
|
|
||||||
}
|
|
||||||
// TODO: fallback case + RB, TL, etc
|
|
||||||
}),
|
|
||||||
} as FcoseLayoutOptions).run();
|
} as FcoseLayoutOptions).run();
|
||||||
cy.ready((e) => {
|
cy.ready((e) => {
|
||||||
log.info('Ready', e);
|
log.info('Ready', e);
|
||||||
@@ -285,6 +272,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
|||||||
const services = db.getServices();
|
const services = db.getServices();
|
||||||
const groups = db.getGroups();
|
const groups = db.getGroups();
|
||||||
const lines = db.getLines();
|
const lines = db.getLines();
|
||||||
|
const ds = db.getDataStructures();
|
||||||
console.log('Services: ', services);
|
console.log('Services: ', services);
|
||||||
console.log('Lines: ', lines);
|
console.log('Lines: ', lines);
|
||||||
console.log('Groups: ', groups);
|
console.log('Groups: ', groups);
|
||||||
@@ -302,7 +290,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
|||||||
|
|
||||||
drawServices(db, servicesElem, services, conf);
|
drawServices(db, servicesElem, services, conf);
|
||||||
|
|
||||||
const cy = await layoutArchitecture(services, groups, lines);
|
const cy = await layoutArchitecture(services, groups, lines, ds);
|
||||||
console.log(cy.nodes().map(node => ({a: node.data()})));
|
console.log(cy.nodes().map(node => ({a: node.data()})));
|
||||||
|
|
||||||
drawEdges(edgesElem, cy);
|
drawEdges(edgesElem, cy);
|
||||||
|
@@ -3,29 +3,104 @@ import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
|||||||
import type { D3Element } from '../../mermaidAPI.js';
|
import type { D3Element } from '../../mermaidAPI.js';
|
||||||
|
|
||||||
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
||||||
|
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>
|
||||||
|
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>
|
||||||
|
export const ArchitectureDirectionName = {
|
||||||
|
'L': 'left',
|
||||||
|
'R': 'right',
|
||||||
|
'T': 'top',
|
||||||
|
'B': 'bottom',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const getOppositeArchitectureDirection = function(x: ArchitectureDirection): ArchitectureDirection {
|
||||||
|
if (isArchitectureDirectionX(x)) {
|
||||||
|
return x === 'L' ? 'R' : 'L'
|
||||||
|
} else {
|
||||||
|
return x === 'T' ? 'B' : 'T'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isArchitectureDirection = function (x: unknown): x is ArchitectureDirection {
|
export const isArchitectureDirection = function (x: unknown): x is ArchitectureDirection {
|
||||||
const temp = x as ArchitectureDirection;
|
const temp = x as ArchitectureDirection;
|
||||||
return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B';
|
return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isArchitectureDirectionX = function (
|
export const isArchitectureDirectionX = function (
|
||||||
x: ArchitectureDirection
|
x: ArchitectureDirection
|
||||||
): x is Extract<ArchitectureDirection, 'L' | 'R'> {
|
): x is ArchitectureDirectionX {
|
||||||
const temp = x as Extract<ArchitectureDirection, 'L' | 'R'>;
|
const temp = x as ArchitectureDirectionX;
|
||||||
return temp === 'L' || temp === 'R';
|
return temp === 'L' || temp === 'R';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isArchitectureDirectionY = function (
|
export const isArchitectureDirectionY = function (
|
||||||
x: ArchitectureDirection
|
x: ArchitectureDirection
|
||||||
): x is Extract<ArchitectureDirection, 'T' | 'B'> {
|
): x is ArchitectureDirectionY {
|
||||||
const temp = x as Extract<ArchitectureDirection, 'T' | 'B'>;
|
const temp = x as ArchitectureDirectionY;
|
||||||
return temp === 'T' || temp === 'B';
|
return temp === 'T' || temp === 'B';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArchitectureDirectionXY = function (
|
||||||
|
a: ArchitectureDirection,
|
||||||
|
b: ArchitectureDirection,
|
||||||
|
) {
|
||||||
|
const aX_bY = isArchitectureDirectionX(a) && isArchitectureDirectionY(b);
|
||||||
|
const aY_bX = isArchitectureDirectionY(a) && isArchitectureDirectionX(b);
|
||||||
|
return aX_bY || aY_bX;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains LL, RR, TT, BB which are impossible conections
|
||||||
|
*/
|
||||||
|
export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`
|
||||||
|
export type ArchitectureDirectionPair = Exclude<InvalidArchitectureDirectionPair, 'LL' | 'RR' | 'TT' | 'BB'>
|
||||||
|
export const isValidArchitectureDirectionPair = function(x: InvalidArchitectureDirectionPair): x is ArchitectureDirectionPair {
|
||||||
|
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB'
|
||||||
|
}
|
||||||
|
export type ArchitectureDirectionPairMap = {
|
||||||
|
[key in ArchitectureDirectionPair]?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pair of the directions of each side of an edge. This function should be used instead of manually creating it to ensure that the source is always the first character.
|
||||||
|
*
|
||||||
|
* Note: Undefined is returned when sourceDir and targetDir are the same. In theory this should never happen since the diagram parser throws an error if a user defines it as such.
|
||||||
|
* @param sourceDir
|
||||||
|
* @param targetDir
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getArchitectureDirectionPair = function (sourceDir: ArchitectureDirection, targetDir: ArchitectureDirection): ArchitectureDirectionPair | undefined {
|
||||||
|
const pair: `${ArchitectureDirection}${ArchitectureDirection}` = `${sourceDir}${targetDir}`;
|
||||||
|
return isValidArchitectureDirectionPair(pair) ? pair : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftPositionByArchitectureDirectionPair = function ([x, y]: number[], pair: ArchitectureDirectionPair): number[] {
|
||||||
|
const lhs = pair[0] as ArchitectureDirection;
|
||||||
|
const rhs = pair[1] as ArchitectureDirection;
|
||||||
|
console.log(`${pair}: (${x},${y})`);
|
||||||
|
if (isArchitectureDirectionX(lhs)) {
|
||||||
|
if (isArchitectureDirectionY(rhs)) {
|
||||||
|
return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)]
|
||||||
|
} else {
|
||||||
|
return [x + (lhs === 'L' ? -1 : 1), y]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isArchitectureDirectionX(rhs)) {
|
||||||
|
return [x + (rhs === 'L' ? 1 : -1), y + (lhs === 'T' ? 1 : -1)]
|
||||||
|
} else {
|
||||||
|
return [x, y + (lhs === 'T' ? 1 : -1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface ArchitectureStyleOptions {
|
export interface ArchitectureStyleOptions {
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArchitectureService {
|
export interface ArchitectureService {
|
||||||
id: string;
|
id: string;
|
||||||
|
edges: ArchitectureLine[];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
in?: string;
|
in?: string;
|
||||||
@@ -66,12 +141,21 @@ export interface ArchitectureDB extends DiagramDB {
|
|||||||
getLines: () => ArchitectureLine[];
|
getLines: () => ArchitectureLine[];
|
||||||
setElementForId: (id: string, element: D3Element) => void;
|
setElementForId: (id: string, element: D3Element) => void;
|
||||||
getElementById: (id: string) => D3Element;
|
getElementById: (id: string) => D3Element;
|
||||||
|
getDataStructures: () => ArchitectureDataStructures;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArchitectureAdjacencyList = {[id: string]: ArchitectureDirectionPairMap}
|
||||||
|
export type ArchitectureSpatialMap = Record<string, number[]>
|
||||||
|
export type ArchitectureDataStructures = {
|
||||||
|
adjList: ArchitectureAdjacencyList;
|
||||||
|
spatialMap: ArchitectureSpatialMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArchitectureFields {
|
export interface ArchitectureFields {
|
||||||
services: ArchitectureService[];
|
services: Record<string, ArchitectureService>;
|
||||||
groups: ArchitectureGroup[];
|
groups: ArchitectureGroup[];
|
||||||
lines: ArchitectureLine[];
|
lines: ArchitectureLine[];
|
||||||
registeredIds: Record<string, 'service' | 'group'>;
|
registeredIds: Record<string, 'service' | 'group'>;
|
||||||
|
datastructures?: ArchitectureDataStructures;
|
||||||
config: ArchitectureDiagramConfig;
|
config: ArchitectureDiagramConfig;
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import type { D3Element } from '../../mermaidAPI.js';
|
|||||||
import { createText } from '../../rendering-util/createText.js';
|
import { createText } from '../../rendering-util/createText.js';
|
||||||
import type {
|
import type {
|
||||||
ArchitectureDB,
|
ArchitectureDB,
|
||||||
|
ArchitectureDirection,
|
||||||
ArchitectureService,
|
ArchitectureService,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import type { MermaidConfig } from '../../config.type.js';
|
import type { MermaidConfig } from '../../config.type.js';
|
||||||
@@ -10,7 +11,16 @@ import { log } from '../../logger.js';
|
|||||||
import { getIcon, isIconNameInUse } from '../../rendering-util/svgRegister.js';
|
import { getIcon, isIconNameInUse } from '../../rendering-util/svgRegister.js';
|
||||||
import { getConfigField } from './architectureDb.js';
|
import { getConfigField } from './architectureDb.js';
|
||||||
|
|
||||||
|
|
||||||
declare module 'cytoscape' {
|
declare module 'cytoscape' {
|
||||||
|
type _EdgeSingularData = {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
sourceDir: ArchitectureDirection;
|
||||||
|
target: string;
|
||||||
|
targetDir: ArchitectureDirection;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
interface EdgeSingular {
|
interface EdgeSingular {
|
||||||
_private: {
|
_private: {
|
||||||
bodyBounds: unknown;
|
bodyBounds: unknown;
|
||||||
@@ -23,6 +33,9 @@ declare module 'cytoscape' {
|
|||||||
endY: number;
|
endY: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// data: (() => _EdgeSingularData) | (<T extends keyof _EdgeSingularData>(key: T) => _EdgeSingularData[T])
|
||||||
|
data(): _EdgeSingularData
|
||||||
|
data<T extends keyof _EdgeSingularData>(key: T): _EdgeSingularData[T]
|
||||||
}
|
}
|
||||||
interface NodeSingular {
|
interface NodeSingular {
|
||||||
_private: {
|
_private: {
|
||||||
@@ -84,6 +97,7 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) {
|
|||||||
const data = node.data();
|
const data = node.data();
|
||||||
if (data.type === 'group') {
|
if (data.type === 'group') {
|
||||||
const { h, w, x1, x2, y1, y2 } = node.boundingBox();
|
const { h, w, x1, x2, y1, y2 } = node.boundingBox();
|
||||||
|
console.log(`Draw group (${data.id}): pos=(${x1}, ${y1}), dim=(${w}, ${h})`)
|
||||||
let bkgElem = groupsEl
|
let bkgElem = groupsEl
|
||||||
.append('rect')
|
.append('rect')
|
||||||
.attr('x', x1 + halfIconSize)
|
.attr('x', x1 + halfIconSize)
|
||||||
@@ -122,7 +136,7 @@ export const drawService = function (
|
|||||||
const textElem = serviceElem.append('g');
|
const textElem = serviceElem.append('g');
|
||||||
createText(textElem, service.title, {
|
createText(textElem, service.title, {
|
||||||
useHtmlLabels: false,
|
useHtmlLabels: false,
|
||||||
width: 110,
|
width: iconSize * 1.5,
|
||||||
classes: 'architecture-service-label',
|
classes: 'architecture-service-label',
|
||||||
});
|
});
|
||||||
textElem
|
textElem
|
||||||
@@ -133,16 +147,16 @@ export const drawService = function (
|
|||||||
|
|
||||||
textElem.attr(
|
textElem.attr(
|
||||||
'transform',
|
'transform',
|
||||||
// TODO: dynamic size
|
|
||||||
'translate(' + (iconSize / 2) + ', ' + iconSize + ')'
|
'translate(' + (iconSize / 2) + ', ' + iconSize + ')'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bkgElem = serviceElem.append('g');
|
let bkgElem = serviceElem.append('g');
|
||||||
if (service.icon) {
|
if (service.icon) {
|
||||||
if (!isIconNameInUse(service.icon)) {
|
// TODO: should a warning be given to end-users saying which icon names are available?
|
||||||
throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
|
// if (!isIconNameInUse(service.icon)) {
|
||||||
}
|
// throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
|
||||||
|
// }
|
||||||
bkgElem = getIcon(service.icon)?.(bkgElem, iconSize);
|
bkgElem = getIcon(service.icon)?.(bkgElem, iconSize);
|
||||||
} else {
|
} else {
|
||||||
bkgElem
|
bkgElem
|
||||||
@@ -157,7 +171,7 @@ export const drawService = function (
|
|||||||
const { width, height } = serviceElem._groups[0][0].getBBox();
|
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||||
service.width = width;
|
service.width = width;
|
||||||
service.height = height;
|
service.height = height;
|
||||||
|
console.log(`Draw service (${service.id})`)
|
||||||
db.setElementForId(service.id, serviceElem);
|
db.setElementForId(service.id, serviceElem);
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user