mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-26 10:49:38 +02:00
feat(arch): improved positioning system to better handle edge cases
This commit is contained in:
@@ -5,9 +5,11 @@ import type {
|
||||
ArchitectureGroup,
|
||||
ArchitectureDirection,
|
||||
ArchitectureLine,
|
||||
ArchitectureDirectionPairMap,
|
||||
ArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { isArchitectureDirection } from './architectureTypes.js';
|
||||
import { getArchitectureDirectionPair, isArchitectureDirection, shiftPositionByArchitectureDirectionPair } from './architectureTypes.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@@ -24,7 +26,7 @@ import type { D3Element } from '../../mermaidAPI.js';
|
||||
export const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
export const DEFAULT_ARCHITECTURE_DB: ArchitectureFields = {
|
||||
services: [],
|
||||
services: {},
|
||||
groups: [],
|
||||
lines: [],
|
||||
registeredIds: {},
|
||||
@@ -35,18 +37,20 @@ let services = DEFAULT_ARCHITECTURE_DB.services;
|
||||
let groups = DEFAULT_ARCHITECTURE_DB.groups;
|
||||
let lines = DEFAULT_ARCHITECTURE_DB.lines;
|
||||
let registeredIds = DEFAULT_ARCHITECTURE_DB.registeredIds;
|
||||
let datastructures = DEFAULT_ARCHITECTURE_DB.datastructures;
|
||||
let elements: Record<string, D3Element> = {};
|
||||
|
||||
const clear = (): void => {
|
||||
services = structuredClone(DEFAULT_ARCHITECTURE_DB.services);
|
||||
groups = structuredClone(DEFAULT_ARCHITECTURE_DB.groups);
|
||||
lines = structuredClone(DEFAULT_ARCHITECTURE_DB.lines);
|
||||
registeredIds = structuredClone(DEFAULT_ARCHITECTURE_DB.registeredIds)
|
||||
registeredIds = structuredClone(DEFAULT_ARCHITECTURE_DB.registeredIds);
|
||||
datastructures = undefined;
|
||||
elements = {};
|
||||
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;
|
||||
if (registeredIds[id] !== undefined) {
|
||||
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';
|
||||
|
||||
services.push({
|
||||
services[id] = {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
edges: [],
|
||||
in: inside,
|
||||
});
|
||||
};
|
||||
};
|
||||
const getServices = (): ArchitectureService[] => services;
|
||||
|
||||
const getServices = (): ArchitectureService[] => Object.values(services);
|
||||
|
||||
const addGroup = function (id: string, opts: Omit<ArchitectureGroup, 'id'> = {}) {
|
||||
const { icon, in: inside, title } = opts;
|
||||
@@ -100,7 +106,63 @@ const addGroup = function (id: string, opts: Omit<ArchitectureGroup, 'id'> = {})
|
||||
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 (
|
||||
lhs_id: string,
|
||||
@@ -110,7 +172,6 @@ const addLine = function (
|
||||
opts: Omit<ArchitectureLine, 'lhs_id' | 'lhs_dir' | 'rhs_id' | 'rhs_dir'> = {}
|
||||
) {
|
||||
const { title, lhs_into, rhs_into } = opts;
|
||||
|
||||
if (!isArchitectureDirection(lhs_dir)) {
|
||||
throw new Error(
|
||||
`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}`
|
||||
);
|
||||
}
|
||||
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_dir,
|
||||
rhs_id,
|
||||
@@ -130,7 +201,12 @@ const addLine = function (
|
||||
title,
|
||||
lhs_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;
|
||||
|
||||
@@ -156,6 +232,7 @@ export const db: ArchitectureDB = {
|
||||
getLines,
|
||||
setElementForId,
|
||||
getElementById,
|
||||
getDataStructures,
|
||||
};
|
||||
|
||||
function getConfigField<T extends keyof ArchitectureDiagramConfig>(field: T): Required<ArchitectureDiagramConfig>[T] {
|
||||
|
@@ -14,12 +14,18 @@ import {
|
||||
type ArchitectureLine,
|
||||
type ArchitectureService,
|
||||
isArchitectureDirectionY,
|
||||
ArchitectureDataStructures,
|
||||
ArchitectureDirectionPair,
|
||||
isArchitectureDirectionXY,
|
||||
ArchitectureDirectionName,
|
||||
getOppositeArchitectureDirection,
|
||||
} from './architectureTypes.js';
|
||||
import { select } from 'd3';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { D3Element } from '../../mermaidAPI.js';
|
||||
import { drawEdges, drawGroups, drawService } from './svgDraw.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import { X } from 'vitest/dist/reporters-5f784f42.js';
|
||||
|
||||
cytoscape.use(fcose);
|
||||
|
||||
@@ -97,7 +103,8 @@ function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) {
|
||||
function layoutArchitecture(
|
||||
services: ArchitectureService[],
|
||||
groups: ArchitectureGroup[],
|
||||
lines: ArchitectureLine[]
|
||||
lines: ArchitectureLine[],
|
||||
{adjList, spatialMap}: ArchitectureDataStructures
|
||||
): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
@@ -151,72 +158,76 @@ function layoutArchitecture(
|
||||
addServices(services, cy);
|
||||
addEdges(lines, cy);
|
||||
|
||||
/**
|
||||
* Merge alignment pairs together if they share a common node.
|
||||
*
|
||||
* Example: [["a", "b"], ["b", "c"], ["d", "e"]] -> [["a", "b", "c"], ["d", "e"]]
|
||||
*/
|
||||
const mergeAlignments = (orig: string[][]): string[][] => {
|
||||
if (orig.length < 1) return orig;
|
||||
console.log('===== mergeAlignments =====');
|
||||
console.log('Start: ', orig);
|
||||
// Mapping of discovered ids to their index in the new alignment array
|
||||
const map: Record<string, number> = {};
|
||||
const newAlignments: string[][] = [orig[0]];
|
||||
map[orig[0][0]] = 0;
|
||||
map[orig[0][1]] = 0;
|
||||
orig = orig.slice(1);
|
||||
while (orig.length > 0) {
|
||||
const pair = orig[0];
|
||||
const pairLHSIdx = map[pair[0]];
|
||||
const pairRHSIdx = map[pair[1]];
|
||||
console.log(pair);
|
||||
console.log(map);
|
||||
console.log(newAlignments);
|
||||
// If neither id appears in the new array, add the pair to the new array
|
||||
if (pairLHSIdx === undefined && pairRHSIdx === undefined) {
|
||||
newAlignments.push(pair);
|
||||
map[pair[0]] = newAlignments.length - 1;
|
||||
map[pair[1]] = newAlignments.length - 1;
|
||||
// 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) {
|
||||
newAlignments[pairRHSIdx].push(pair[0]);
|
||||
map[pair[0]] = pairRHSIdx;
|
||||
// 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
|
||||
} else if (pairRHSIdx === undefined) {
|
||||
newAlignments[pairLHSIdx].push(pair[1]);
|
||||
map[pair[1]] = pairLHSIdx;
|
||||
// If both ids already have been added to the new array and their index is different, merge all 3 arrays
|
||||
} else if (pairLHSIdx != pairRHSIdx) {
|
||||
console.log('ELSE');
|
||||
newAlignments.push(pair);
|
||||
}
|
||||
orig = orig.slice(1);
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
const [horizontalAlignments, verticalAlignments] = (() => {
|
||||
const _horizontalAlignments: Record<number, string[]> = {}
|
||||
const _verticalAlignments: Record<number, string[]> = {}
|
||||
// Group service ids in an object with their x and y coordinate as the key
|
||||
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
||||
if (!_horizontalAlignments[y]) _horizontalAlignments[y] = [];
|
||||
if (!_verticalAlignments[x]) _verticalAlignments[x] = [];
|
||||
_horizontalAlignments[y].push(id);
|
||||
_verticalAlignments[x].push(id);
|
||||
})
|
||||
|
||||
// Merge the values of each object into a list if the inner list has at least 2 elements
|
||||
return [
|
||||
Object.values(_horizontalAlignments).filter(arr => arr.length > 1),
|
||||
Object.values(_verticalAlignments).filter(arr => arr.length > 1)
|
||||
]
|
||||
})();
|
||||
|
||||
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||
const relativeConstraints = (() => {
|
||||
const _relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []
|
||||
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`
|
||||
const strToPos = (pos: string) => pos.split(',').map(p => parseInt(p));
|
||||
const invSpatialMap = Object.fromEntries(Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]))
|
||||
console.log('===== invSpatialMap =====')
|
||||
console.log(invSpatialMap);
|
||||
|
||||
// perform BFS
|
||||
const queue = [posToStr([0,0])];
|
||||
const visited: Record<string, number> = {};
|
||||
const directions: Record<ArchitectureDirection, number[]> = {
|
||||
"L": [-1, 0],
|
||||
"R": [1, 0],
|
||||
"T": [0, 1],
|
||||
"B": [0, -1]
|
||||
}
|
||||
|
||||
console.log('End: ', newAlignments);
|
||||
console.log('===========================');
|
||||
|
||||
return newAlignments;
|
||||
};
|
||||
|
||||
const horizontalAlignments = cy
|
||||
.edges()
|
||||
.filter(
|
||||
(edge) =>
|
||||
isArchitectureDirectionX(edge.data('sourceDir')) &&
|
||||
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')]);
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return _relativeConstraints;
|
||||
})();
|
||||
console.log(`Horizontal Alignments:`)
|
||||
console.log(horizontalAlignments);
|
||||
console.log(`Vertical Alignments:`)
|
||||
console.log(verticalAlignments);
|
||||
console.log(`Relative Alignments:`)
|
||||
console.log(relativeConstraints);
|
||||
|
||||
cy.layout({
|
||||
name: 'fcose',
|
||||
@@ -242,34 +253,10 @@ function layoutArchitecture(
|
||||
return elasticity
|
||||
},
|
||||
alignmentConstraint: {
|
||||
horizontal: mergeAlignments(horizontalAlignments),
|
||||
vertical: mergeAlignments(verticalAlignments),
|
||||
horizontal: horizontalAlignments,
|
||||
vertical: verticalAlignments,
|
||||
},
|
||||
relativePlacementConstraint: cy.edges().map((edge) => {
|
||||
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
|
||||
}),
|
||||
relativePlacementConstraint: relativeConstraints
|
||||
} as FcoseLayoutOptions).run();
|
||||
cy.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 groups = db.getGroups();
|
||||
const lines = db.getLines();
|
||||
const ds = db.getDataStructures();
|
||||
console.log('Services: ', services);
|
||||
console.log('Lines: ', lines);
|
||||
console.log('Groups: ', groups);
|
||||
@@ -302,7 +290,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
|
||||
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()})));
|
||||
|
||||
drawEdges(edgesElem, cy);
|
||||
|
@@ -3,29 +3,104 @@ import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import type { D3Element } from '../../mermaidAPI.js';
|
||||
|
||||
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 {
|
||||
const temp = x as ArchitectureDirection;
|
||||
return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B';
|
||||
};
|
||||
|
||||
export const isArchitectureDirectionX = function (
|
||||
x: ArchitectureDirection
|
||||
): x is Extract<ArchitectureDirection, 'L' | 'R'> {
|
||||
const temp = x as Extract<ArchitectureDirection, 'L' | 'R'>;
|
||||
): x is ArchitectureDirectionX {
|
||||
const temp = x as ArchitectureDirectionX;
|
||||
return temp === 'L' || temp === 'R';
|
||||
};
|
||||
|
||||
export const isArchitectureDirectionY = function (
|
||||
x: ArchitectureDirection
|
||||
): x is Extract<ArchitectureDirection, 'T' | 'B'> {
|
||||
const temp = x as Extract<ArchitectureDirection, 'T' | 'B'>;
|
||||
): x is ArchitectureDirectionY {
|
||||
const temp = x as ArchitectureDirectionY;
|
||||
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 {
|
||||
fontFamily: string;
|
||||
}
|
||||
|
||||
export interface ArchitectureService {
|
||||
id: string;
|
||||
edges: ArchitectureLine[];
|
||||
icon?: string;
|
||||
title?: string;
|
||||
in?: string;
|
||||
@@ -66,12 +141,21 @@ export interface ArchitectureDB extends DiagramDB {
|
||||
getLines: () => ArchitectureLine[];
|
||||
setElementForId: (id: string, element: D3Element) => void;
|
||||
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 {
|
||||
services: ArchitectureService[];
|
||||
services: Record<string, ArchitectureService>;
|
||||
groups: ArchitectureGroup[];
|
||||
lines: ArchitectureLine[];
|
||||
registeredIds: Record<string, 'service' | 'group'>;
|
||||
datastructures?: ArchitectureDataStructures;
|
||||
config: ArchitectureDiagramConfig;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import type { D3Element } from '../../mermaidAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import type {
|
||||
ArchitectureDB,
|
||||
ArchitectureDirection,
|
||||
ArchitectureService,
|
||||
} from './architectureTypes.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 { getConfigField } from './architectureDb.js';
|
||||
|
||||
|
||||
declare module 'cytoscape' {
|
||||
type _EdgeSingularData = {
|
||||
id: string;
|
||||
source: string;
|
||||
sourceDir: ArchitectureDirection;
|
||||
target: string;
|
||||
targetDir: ArchitectureDirection;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
@@ -23,6 +33,9 @@ declare module 'cytoscape' {
|
||||
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 {
|
||||
_private: {
|
||||
@@ -84,6 +97,7 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) {
|
||||
const data = node.data();
|
||||
if (data.type === 'group') {
|
||||
const { h, w, x1, x2, y1, y2 } = node.boundingBox();
|
||||
console.log(`Draw group (${data.id}): pos=(${x1}, ${y1}), dim=(${w}, ${h})`)
|
||||
let bkgElem = groupsEl
|
||||
.append('rect')
|
||||
.attr('x', x1 + halfIconSize)
|
||||
@@ -122,7 +136,7 @@ export const drawService = function (
|
||||
const textElem = serviceElem.append('g');
|
||||
createText(textElem, service.title, {
|
||||
useHtmlLabels: false,
|
||||
width: 110,
|
||||
width: iconSize * 1.5,
|
||||
classes: 'architecture-service-label',
|
||||
});
|
||||
textElem
|
||||
@@ -133,16 +147,16 @@ export const drawService = function (
|
||||
|
||||
textElem.attr(
|
||||
'transform',
|
||||
// TODO: dynamic size
|
||||
'translate(' + (iconSize / 2) + ', ' + iconSize + ')'
|
||||
);
|
||||
}
|
||||
|
||||
let bkgElem = serviceElem.append('g');
|
||||
if (service.icon) {
|
||||
if (!isIconNameInUse(service.icon)) {
|
||||
throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
|
||||
}
|
||||
// TODO: should a warning be given to end-users saying which icon names are available?
|
||||
// if (!isIconNameInUse(service.icon)) {
|
||||
// throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
|
||||
// }
|
||||
bkgElem = getIcon(service.icon)?.(bkgElem, iconSize);
|
||||
} else {
|
||||
bkgElem
|
||||
@@ -157,7 +171,7 @@ export const drawService = function (
|
||||
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||
service.width = width;
|
||||
service.height = height;
|
||||
|
||||
console.log(`Draw service (${service.id})`)
|
||||
db.setElementForId(service.id, serviceElem);
|
||||
return 0;
|
||||
};
|
||||
|
Reference in New Issue
Block a user