feat(arch): improved positioning system to better handle edge cases

This commit is contained in:
NicolasNewman
2024-04-03 13:45:41 -05:00
parent f47bbee24a
commit 36f52be3bf
4 changed files with 279 additions and 116 deletions

View File

@@ -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] {

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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;
}; };