6774: update architecture diagram to reflect new class-based DB structure

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
omkarht
2025-07-29 15:21:34 +05:30
parent 0cc0b63e52
commit 1a4b8662cf
6 changed files with 364 additions and 349 deletions

View File

@@ -1,21 +1,12 @@
import { it, describe, expect } from 'vitest'; import { it, describe, expect } from 'vitest';
import { db } from './architectureDb.js';
import { parser } from './architectureParser.js'; import { parser } from './architectureParser.js';
import { ArchitectureDB } from './architectureDb.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getServices,
getGroups,
getEdges,
getJunctions,
} = db;
describe('architecture diagrams', () => { describe('architecture diagrams', () => {
let db: ArchitectureDB;
beforeEach(() => { beforeEach(() => {
clear(); db = new ArchitectureDB();
// @ts-expect-error since type is set to undefined we will have error
parser.parser?.yy = db;
}); });
describe('architecture diagram definitions', () => { describe('architecture diagram definitions', () => {
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
it('should handle title on the first line', async () => { it('should handle title on the first line', async () => {
const str = `architecture-beta title Simple Architecture Diagram`; const str = `architecture-beta title Simple Architecture Diagram`;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
}); });
it('should handle title on another line', async () => { it('should handle title on another line', async () => {
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
title Simple Architecture Diagram title Simple Architecture Diagram
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram');
}); });
it('should handle accessibility title and description', async () => { it('should handle accessibility title and description', async () => {
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
accDescr: Accessibility Description accDescr: Accessibility Description
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccTitle()).toBe('Accessibility Title'); expect(db.getAccTitle()).toBe('Accessibility Title');
expect(getAccDescription()).toBe('Accessibility Description'); expect(db.getAccDescription()).toBe('Accessibility Description');
}); });
it('should handle multiline accessibility description', async () => { it('should handle multiline accessibility description', async () => {
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
} }
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccDescription()).toBe('Accessibility Description'); expect(db.getAccDescription()).toBe('Accessibility Description');
}); });
}); });
}); });

View File

@@ -1,8 +1,9 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js'; import DEFAULT_CONFIG from '../../defaultConfig.js';
import { getConfig as commonGetConfig } from '../../config.js'; import type { DiagramDB } from '../../diagram-api/types.js';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { ImperativeState } from '../../utils/imperativeState.js'; import { cleanAndMerge } from '../../utils.js';
import { import {
clear as commonClear, clear as commonClear,
getAccDescription, getAccDescription,
@@ -14,7 +15,6 @@ import {
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import type { import type {
ArchitectureAlignment, ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair, ArchitectureDirectionPair,
ArchitectureDirectionPairMap, ArchitectureDirectionPairMap,
ArchitectureEdge, ArchitectureEdge,
@@ -33,55 +33,61 @@ import {
isArchitectureService, isArchitectureService,
shiftPositionByArchitectureDirectionPair, shiftPositionByArchitectureDirectionPair,
} from './architectureTypes.js'; } from './architectureTypes.js';
import { cleanAndMerge } from '../../utils.js';
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> = const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture; DEFAULT_CONFIG.architecture;
export class ArchitectureDB implements DiagramDB {
private nodes: Record<string, ArchitectureNode> = {};
private groups: Record<string, ArchitectureGroup> = {};
private edges: ArchitectureEdge[] = [];
private registeredIds: Record<string, 'node' | 'group'> = {};
// private config: Required<ArchitectureDiagramConfig> = DEFAULT_ARCHITECTURE_CONFIG;
private dataStructures?: ArchitectureState['dataStructures'];
private elements: Record<string, D3Element> = {};
const state = new ImperativeState<ArchitectureState>(() => ({ constructor() {
nodes: {}, this.clear();
groups: {}, }
edges: [],
registeredIds: {},
config: DEFAULT_ARCHITECTURE_CONFIG,
dataStructures: undefined,
elements: {},
}));
const clear = (): void => { public clear(): void {
state.reset(); this.nodes = {};
this.groups = {};
this.edges = [];
this.registeredIds = {};
this.dataStructures = undefined;
this.elements = {};
commonClear(); commonClear();
}; }
const addService = function ({ public addService({
id, id,
icon, icon,
in: parent, in: parent,
title, title,
iconText, iconText,
}: Omit<ArchitectureService, 'edges'>) { }: Omit<ArchitectureService, 'edges'>): void {
if (state.records.registeredIds[id] !== undefined) { if (this.registeredIds[id] !== undefined) {
throw new Error( throw new Error(
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}` `The service id [${id}] is already in use by another ${this.registeredIds[id]}`
); );
} }
if (parent !== undefined) { if (parent !== undefined) {
if (id === parent) { if (id === parent) {
throw new Error(`The service [${id}] cannot be placed within itself`); throw new Error(`The service [${id}] cannot be placed within itself`);
} }
if (state.records.registeredIds[parent] === undefined) { if (this.registeredIds[parent] === undefined) {
throw new Error( throw new Error(
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service` `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
); );
} }
if (state.records.registeredIds[parent] === 'node') { if (this.registeredIds[parent] === 'node') {
throw new Error(`The service [${id}]'s parent is not a group`); throw new Error(`The service [${id}]'s parent is not a group`);
} }
} }
state.records.registeredIds[id] = 'node'; this.registeredIds[id] = 'node';
state.records.nodes[id] = { this.nodes[id] = {
id, id,
type: 'service', type: 'service',
icon, icon,
@@ -90,63 +96,68 @@ const addService = function ({
edges: [], edges: [],
in: parent, in: parent,
}; };
}; }
const getServices = (): ArchitectureService[] => public getServices(): ArchitectureService[] {
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService); return Object.values(this.nodes).filter(isArchitectureService);
}
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) { public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
state.records.registeredIds[id] = 'node'; this.registeredIds[id] = 'node';
state.records.nodes[id] = { this.nodes[id] = {
id, id,
type: 'junction', type: 'junction',
edges: [], edges: [],
in: parent, in: parent,
}; };
}; }
const getJunctions = (): ArchitectureJunction[] => public getJunctions(): ArchitectureJunction[] {
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction); return Object.values(this.nodes).filter(isArchitectureJunction);
}
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes); public getNodes(): ArchitectureNode[] {
return Object.values(this.nodes);
}
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id]; public getNode(id: string): ArchitectureNode | null {
return this.nodes[id] ?? null;
}
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) { public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
if (state.records.registeredIds[id] !== undefined) { if (this.registeredIds?.[id] !== undefined) {
throw new Error( throw new Error(
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}` `The group id [${id}] is already in use by another ${this.registeredIds[id]}`
); );
} }
if (parent !== undefined) { if (parent !== undefined) {
if (id === parent) { if (id === parent) {
throw new Error(`The group [${id}] cannot be placed within itself`); throw new Error(`The group [${id}] cannot be placed within itself`);
} }
if (state.records.registeredIds[parent] === undefined) { if (this.registeredIds?.[parent] === undefined) {
throw new Error( throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group` `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
); );
} }
if (state.records.registeredIds[parent] === 'node') { if (this.registeredIds?.[parent] === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`); throw new Error(`The group [${id}]'s parent is not a group`);
} }
} }
state.records.registeredIds[id] = 'group'; this.registeredIds[id] = 'group';
state.records.groups[id] = { this.groups[id] = {
id, id,
icon, icon,
title, title,
in: parent, in: parent,
}; };
}; }
const getGroups = (): ArchitectureGroup[] => { public getGroups(): ArchitectureGroup[] {
return Object.values(state.records.groups); return Object.values(this.groups);
}; }
public addEdge({
const addEdge = function ({
lhsId, lhsId,
rhsId, rhsId,
lhsDir, lhsDir,
@@ -156,31 +167,31 @@ const addEdge = function ({
lhsGroup, lhsGroup,
rhsGroup, rhsGroup,
title, title,
}: ArchitectureEdge<string>) { }: ArchitectureEdge): void {
if (!isArchitectureDirection(lhsDir)) { if (!isArchitectureDirection(lhsDir)) {
throw new Error( throw new Error(
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}` `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
); );
} }
if (!isArchitectureDirection(rhsDir)) { if (!isArchitectureDirection(rhsDir)) {
throw new Error( throw new Error(
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}` `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
); );
} }
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) { if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
throw new Error( throw new Error(
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
); );
} }
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) { if (this.nodes[rhsId] === undefined && this.groups[lhsId] === undefined) {
throw new Error( throw new Error(
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` `The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
); );
} }
const lhsGroupId = state.records.nodes[lhsId].in; const lhsGroupId = this.nodes[lhsId].in;
const rhsGroupId = state.records.nodes[rhsId].in; const rhsGroupId = this.nodes[rhsId].in;
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error( throw new Error(
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
@@ -204,22 +215,24 @@ const addEdge = function ({
title, title,
}; };
state.records.edges.push(edge); this.edges.push(edge);
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) { if (this.nodes[lhsId] && this.nodes[rhsId]) {
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]); this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]); this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
}
} }
};
const getEdges = (): ArchitectureEdge[] => state.records.edges; public getEdges(): ArchitectureEdge[] {
return this.edges;
}
/** /**
* Returns the current diagram's adjacency list, spatial map, & group alignments. * Returns the current diagram's adjacency list, spatial map, & group alignments.
* If they have not been created, run the algorithms to generate them. * If they have not been created, run the algorithms to generate them.
* @returns * @returns
*/ */
const getDataStructures = () => { public getDataStructures() {
if (state.records.dataStructures === undefined) { if (this.dataStructures === undefined) {
// Tracks how groups are aligned with one another. Generated while creating the adj list // Tracks how groups are aligned with one another. Generated while creating the adj list
const groupAlignments: Record< const groupAlignments: Record<
string, string,
@@ -229,13 +242,13 @@ const getDataStructures = () => {
// Create an adjacency list of the diagram to perform BFS on // Create an adjacency list of the diagram to perform BFS on
// Outer reduce applied on all services // Outer reduce applied on all services
// Inner reduce applied on the edges for a service // Inner reduce applied on the edges for a service
const adjList = Object.entries(state.records.nodes).reduce< const adjList = Object.entries(this.nodes).reduce<
Record<string, ArchitectureDirectionPairMap> Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => { >((prevOuter, [id, service]) => {
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => { prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
// track the direction groups connect to one another // track the direction groups connect to one another
const lhsGroupId = getNode(edge.lhsId)?.in; const lhsGroupId = this.getNode(edge.lhsId)?.in;
const rhsGroupId = getNode(edge.rhsId)?.in; const rhsGroupId = this.getNode(edge.rhsId)?.in;
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) { if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir); const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
if (alignment !== 'bend') { if (alignment !== 'bend') {
@@ -303,60 +316,51 @@ const getDataStructures = () => {
while (Object.keys(notVisited).length > 0) { while (Object.keys(notVisited).length > 0) {
spatialMaps.push(BFS(Object.keys(notVisited)[0])); spatialMaps.push(BFS(Object.keys(notVisited)[0]));
} }
state.records.dataStructures = { this.dataStructures = {
adjList, adjList,
spatialMaps, spatialMaps,
groupAlignments, groupAlignments,
}; };
} }
return state.records.dataStructures; return this.dataStructures;
}; }
const setElementForId = (id: string, element: D3Element) => { public setElementForId(id: string, element: D3Element): void {
state.records.elements[id] = element; this.elements[id] = element;
}; }
const getElementById = (id: string) => state.records.elements[id];
const getConfig = (): Required<ArchitectureDiagramConfig> => { public getElementById(id: string): D3Element {
const config = cleanAndMerge({ return this.elements[id];
}
public getConfig(): Required<ArchitectureDiagramConfig> {
return cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG, ...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture, ...commonGetConfig().architecture,
}); });
return config; }
};
export const db: ArchitectureDB = { public getConfigField<T extends keyof ArchitectureDiagramConfig>(
clear, field: T
setDiagramTitle, ): Required<ArchitectureDiagramConfig>[T] {
getDiagramTitle, return this.getConfig()[field];
setAccTitle, }
getAccTitle,
setAccDescription,
getAccDescription,
getConfig,
addService, public setAccTitle = setAccTitle;
getServices, public getAccTitle = getAccTitle;
addJunction, public setDiagramTitle = setDiagramTitle;
getJunctions, public getDiagramTitle = getDiagramTitle;
getNodes, public getAccDescription = getAccDescription;
getNode, public setAccDescription = setAccDescription;
addGroup, }
getGroups,
addEdge,
getEdges,
setElementForId,
getElementById,
getDataStructures,
};
/** /**
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
* @param field - the config field to access * @param field - the config field to access
* @returns * @returns
*/ */
export function getConfigField<T extends keyof ArchitectureDiagramConfig>( // export function getConfigField<T extends key of ArchitectureDiagramConfig>(
field: T // field: T
): Required<ArchitectureDiagramConfig>[T] { // ): Required<ArchitectureDiagramConfig>[T] {
return getConfig()[field]; // return db.getConfig()[field];
} // }

View File

@@ -1,12 +1,14 @@
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './architectureParser.js'; import { parser } from './architectureParser.js';
import { db } from './architectureDb.js'; import { ArchitectureDB } from './architectureDb.js';
import styles from './architectureStyles.js'; import styles from './architectureStyles.js';
import { renderer } from './architectureRenderer.js'; import { renderer } from './architectureRenderer.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
parser, parser,
db, get db() {
return new ArchitectureDB();
},
renderer, renderer,
styles, styles,
}; };

View File

@@ -1,24 +1,33 @@
import type { Architecture } from '@mermaid-js/parser'; import type { Architecture } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser'; import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js'; import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js'; import { populateCommonDb } from '../common/populateCommonDb.js';
import type { ArchitectureDB } from './architectureTypes.js'; import { ArchitectureDB } from './architectureDb.js';
import { db } from './architectureDb.js';
const populateDb = (ast: Architecture, db: ArchitectureDB) => { const populateDb = (ast: Architecture, db: ArchitectureDB) => {
populateCommonDb(ast, db); populateCommonDb(ast, db);
ast.groups.map(db.addGroup); ast.groups.map((group) => db.addGroup(group));
ast.services.map((service) => db.addService({ ...service, type: 'service' })); ast.services.map((service) => db.addService({ ...service, type: 'service' }));
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' })); ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type? // @ts-ignore ToDo our parser guarantees the type is L/R/T/B and not string. How to change to union type?
ast.edges.map(db.addEdge); ast.edges.map((edge) => db.addEdge(edge));
}; };
export const parser: ParserDefinition = { export const parser: ParserDefinition = {
parser: {
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
yy: undefined,
},
parse: async (input: string): Promise<void> => { parse: async (input: string): Promise<void> => {
const ast: Architecture = await parse('architecture', input); const ast: Architecture = await parse('architecture', input);
log.debug(ast); log.debug(ast);
const db = parser.parser?.yy;
if (!(db instanceof ArchitectureDB)) {
throw new Error(
'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
);
}
populateDb(ast, db); populateDb(ast, db);
}, },
}; };

View File

@@ -1,4 +1,3 @@
import { registerIconPacks } from '../../rendering-util/icons.js';
import type { Position } from 'cytoscape'; import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape'; import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose'; import type { FcoseLayoutOptions } from 'cytoscape-fcose';
@@ -7,9 +6,10 @@ import { select } from 'd3';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
import type { Diagram } from '../../Diagram.js'; import type { Diagram } from '../../Diagram.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { registerIconPacks } from '../../rendering-util/icons.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { getConfigField } from './architectureDb.js'; import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js'; import { architectureIcons } from './architectureIcons.js';
import type { import type {
ArchitectureAlignment, ArchitectureAlignment,
@@ -22,7 +22,6 @@ import type {
NodeSingularData, NodeSingularData,
} from './architectureTypes.js'; } from './architectureTypes.js';
import { import {
type ArchitectureDB,
type ArchitectureDirection, type ArchitectureDirection,
type ArchitectureEdge, type ArchitectureEdge,
type ArchitectureGroup, type ArchitectureGroup,
@@ -44,7 +43,7 @@ registerIconPacks([
]); ]);
cytoscape.use(fcose); cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core) { function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
services.forEach((service) => { services.forEach((service) => {
cy.add({ cy.add({
group: 'nodes', group: 'nodes',
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
icon: service.icon, icon: service.icon,
label: service.title, label: service.title,
parent: service.in, parent: service.in,
width: getConfigField('iconSize'), width: db.getConfigField('iconSize'),
height: getConfigField('iconSize'), height: db.getConfigField('iconSize'),
} as NodeSingularData, } as NodeSingularData,
classes: 'node-service', classes: 'node-service',
}); });
}); });
} }
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
junctions.forEach((junction) => { junctions.forEach((junction) => {
cy.add({ cy.add({
group: 'nodes', group: 'nodes',
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
type: 'junction', type: 'junction',
id: junction.id, id: junction.id,
parent: junction.in, parent: junction.in,
width: getConfigField('iconSize'), width: db.getConfigField('iconSize'),
height: getConfigField('iconSize'), height: db.getConfigField('iconSize'),
} as NodeSingularData, } as NodeSingularData,
classes: 'node-junction', classes: 'node-junction',
}); });
@@ -192,7 +191,7 @@ function getAlignments(
prev[dir] ??= []; prev[dir] ??= [];
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev
} else if (aGroupId === 'default' || bGroupId === 'default') { } else if (aGroupId === 'default' || bGroupId === 'default') {
// If either of the groups are in the default space (not in a group), use the same behavior as above // If either of the groups are in the default space (not in a group), use the same behaviour as above
prev[dir] ??= []; prev[dir] ??= [];
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
} else { } else {
@@ -257,7 +256,8 @@ function getAlignments(
} }
function getRelativeConstraints( function getRelativeConstraints(
spatialMaps: ArchitectureSpatialMap[] spatialMaps: ArchitectureSpatialMap[],
db: ArchitectureDB
): fcose.FcoseRelativePlacementConstraint[] { ): fcose.FcoseRelativePlacementConstraint[] {
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
@@ -296,7 +296,7 @@ function getRelativeConstraints(
[ArchitectureDirectionName[ [ArchitectureDirectionName[
getOppositeArchitectureDirection(dir as ArchitectureDirection) getOppositeArchitectureDirection(dir as ArchitectureDirection)
]]: currId, ]]: currId,
gap: 1.5 * getConfigField('iconSize'), gap: 1.5 * db.getConfigField('iconSize'),
}); });
} }
}); });
@@ -353,7 +353,7 @@ function layoutArchitecture(
style: { style: {
'text-valign': 'bottom', 'text-valign': 'bottom',
'text-halign': 'center', 'text-halign': 'center',
'font-size': `${getConfigField('fontSize')}px`, 'font-size': `${db.getConfigField('fontSize')}px`,
}, },
}, },
{ {
@@ -375,7 +375,7 @@ function layoutArchitecture(
selector: '.node-group', selector: '.node-group',
style: { style: {
// @ts-ignore Incorrect library types // @ts-ignore Incorrect library types
padding: `${getConfigField('padding')}px`, padding: `${db.getConfigField('padding')}px`,
}, },
}, },
], ],
@@ -393,14 +393,14 @@ function layoutArchitecture(
renderEl.remove(); renderEl.remove();
addGroups(groups, cy); addGroups(groups, cy);
addServices(services, cy); addServices(services, cy, db);
addJunctions(junctions, cy); addJunctions(junctions, cy, db);
addEdges(edges, cy); addEdges(edges, cy);
// Use the spatial map to create alignment arrays for fcose // Use the spatial map to create alignment arrays for fcose
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments); const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
const relativePlacementConstraint = getRelativeConstraints(spatialMaps); const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
const layout = cy.layout({ const layout = cy.layout({
name: 'fcose', name: 'fcose',
@@ -415,7 +415,9 @@ function layoutArchitecture(
const { parent: parentA } = nodeData(nodeA); const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB); const { parent: parentB } = nodeData(nodeB);
const elasticity = const elasticity =
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize'); parentA === parentB
? 1.5 * db.getConfigField('iconSize')
: 0.5 * db.getConfigField('iconSize');
return elasticity; return elasticity;
}, },
edgeElasticity(edge: EdgeSingular) { edgeElasticity(edge: EdgeSingular) {
@@ -535,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds); const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
await drawEdges(edgesElem, cy); await drawEdges(edgesElem, cy, db);
await drawGroups(groupElem, cy); await drawGroups(groupElem, cy, db);
positionNodes(db, cy); positionNodes(db, cy);
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth')); setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
}; };
export const renderer = { draw }; export const renderer = { draw };

View File

@@ -1,9 +1,9 @@
import { getIconSVG } from '../../rendering-util/icons.js';
import type cytoscape from 'cytoscape'; import type cytoscape from 'cytoscape';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import { createText } from '../../rendering-util/createText.js'; import { createText } from '../../rendering-util/createText.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { db, getConfigField } from './architectureDb.js'; import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js'; import { architectureIcons } from './architectureIcons.js';
import { import {
ArchitectureDirectionArrow, ArchitectureDirectionArrow,
@@ -16,14 +16,17 @@ import {
isArchitectureDirectionY, isArchitectureDirectionY,
isArchitecturePairXY, isArchitecturePairXY,
nodeData, nodeData,
type ArchitectureDB,
type ArchitectureJunction, type ArchitectureJunction,
type ArchitectureService, type ArchitectureService,
} from './architectureTypes.js'; } from './architectureTypes.js';
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) { export const drawEdges = async function (
const padding = getConfigField('padding'); edgesEl: D3Element,
const iconSize = getConfigField('iconSize'); cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const iconSize = db.getConfigField('iconSize');
const halfIconSize = iconSize / 2; const halfIconSize = iconSize / 2;
const arrowSize = iconSize / 6; const arrowSize = iconSize / 6;
const halfArrowSize = arrowSize / 2; const halfArrowSize = arrowSize / 2;
@@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core)
); );
}; };
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) { export const drawGroups = async function (
const padding = getConfigField('padding'); groupsEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const groupIconSize = padding * 0.75; const groupIconSize = padding * 0.75;
const fontSize = getConfigField('fontSize'); const fontSize = db.getConfigField('fontSize');
const iconSize = getConfigField('iconSize'); const iconSize = db.getConfigField('iconSize');
const halfIconSize = iconSize / 2; const halfIconSize = iconSize / 2;
await Promise.all( await Promise.all(
@@ -266,7 +273,7 @@ export const drawServices = async function (
): Promise<number> { ): Promise<number> {
for (const service of services) { for (const service of services) {
const serviceElem = elem.append('g'); const serviceElem = elem.append('g');
const iconSize = getConfigField('iconSize'); const iconSize = db.getConfigField('iconSize');
if (service.title) { if (service.title) {
const textElem = serviceElem.append('g'); const textElem = serviceElem.append('g');
@@ -350,7 +357,7 @@ export const drawJunctions = function (
) { ) {
junctions.forEach((junction) => { junctions.forEach((junction) => {
const junctionElem = elem.append('g'); const junctionElem = elem.append('g');
const iconSize = getConfigField('iconSize'); const iconSize = db.getConfigField('iconSize');
const bkgElem = junctionElem.append('g'); const bkgElem = junctionElem.append('g');
bkgElem bkgElem