Skip to content

Commit a3672eb

Browse files
committed
Refactor class layout algorithms
1 parent ce815e0 commit a3672eb

File tree

3 files changed

+60
-175
lines changed

3 files changed

+60
-175
lines changed

src/utils/circle-layouter.ts

Lines changed: 13 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { useUserSettingsStore } from 'explorviz-frontend/src/stores/user-setting
22
import BoxLayout from 'explorviz-frontend/src/view-objects/layout-models/box-layout';
33
import { getAllClassesInApplication } from './application-helpers';
44
import { Application, Package } from './landscape-schemes/structure-data';
5-
import { metricMappingMultipliers } from './settings/default-settings';
6-
import { SelectedClassMetric } from './settings/settings-schemas';
7-
8-
// Constants for class node creation (matching elk-layouter.ts)
9-
const CLASS_PREFIX = 'clss-';
105

116
let APP_LABEL_MARGIN: number;
127
let APP_MARGIN: number;
@@ -31,76 +26,6 @@ export function setCircleLayoutSettings() {
3126
APP_MARGIN = vs.appMargin.value;
3227
}
3328

34-
/**
35-
* Collects all classes from a package and its sub-packages recursively.
36-
* Creates ELK graph nodes for each class, skipping packages.
37-
*/
38-
export function collectAllClassesForCircleLayout(
39-
pkg: Package,
40-
classNodes: any[],
41-
removedComponentIds: Set<string>
42-
) {
43-
pkg.classes.forEach((classModel) => {
44-
if (removedComponentIds.has(classModel.id)) {
45-
return;
46-
}
47-
let widthByMetric = 0;
48-
if (WIDTH_METRIC === SelectedClassMetric.Method) {
49-
widthByMetric =
50-
WIDTH_METRIC_MULTIPLIER *
51-
metricMappingMultipliers['Method Count'] *
52-
classModel.methods.length;
53-
}
54-
55-
let depthByMetric = 0;
56-
if (DEPTH_METRIC === SelectedClassMetric.Method) {
57-
depthByMetric =
58-
DEPTH_METRIC_MULTIPLIER *
59-
metricMappingMultipliers['Method Count'] *
60-
classModel.methods.length;
61-
}
62-
63-
const classNode = {
64-
id: CLASS_PREFIX + classModel.id,
65-
children: [],
66-
width: CLASS_FOOTPRINT + widthByMetric,
67-
height: CLASS_FOOTPRINT + depthByMetric,
68-
};
69-
classNodes.push(classNode);
70-
});
71-
72-
pkg.subPackages.forEach((subPackage) => {
73-
if (!removedComponentIds.has(subPackage.id)) {
74-
collectAllClassesForCircleLayout(
75-
subPackage,
76-
classNodes,
77-
removedComponentIds
78-
);
79-
}
80-
});
81-
}
82-
83-
/**
84-
* Collects all classes from an application for circle layout.
85-
* Returns an array of ELK graph nodes for all classes in the application.
86-
*/
87-
export function collectApplicationClassesForCircleLayout(
88-
application: Application,
89-
removedComponentIds: Set<string>
90-
): any[] {
91-
const allClassNodes: any[] = [];
92-
application.packages.forEach((component) => {
93-
if (!removedComponentIds.has(component.id)) {
94-
collectAllClassesForCircleLayout(
95-
component,
96-
allClassNodes,
97-
removedComponentIds
98-
);
99-
}
100-
});
101-
return allClassNodes;
102-
}
103-
10429
/**
10530
* Collects all package IDs from an application recursively.
10631
* Used to remove package layouts from the layout map when circle layout is enabled.
@@ -130,25 +55,20 @@ export function collectPackageIds(
13055
*/
13156
export function applyCircleLayoutToClasses(
13257
boxLayoutMap: Map<string, BoxLayout>,
133-
applications: Application[],
134-
removedComponentIds: Set<string>
58+
applications: Application[]
13559
) {
13660
setCircleLayoutSettings();
13761
applications.forEach((application) => {
138-
if (removedComponentIds.has(application.id)) {
139-
return;
140-
}
141-
14262
// Get application layout to determine circle size
14363
const appLayout = boxLayoutMap.get(application.id);
14464
if (!appLayout) {
14565
return;
14666
}
14767

148-
// Get all classes in this application
149-
const classes = getAllClassesInApplication(application)
150-
.filter((classModel) => !removedComponentIds.has(classModel.id))
151-
.sort((classA, classB) => classA.fqn!.localeCompare(classB.fqn!));
68+
// Get all classes in applications and sort by fqn
69+
const classes = getAllClassesInApplication(application).sort(
70+
(classA, classB) => classA.fqn!.localeCompare(classB.fqn!)
71+
);
15272

15373
if (classes.length === 0) {
15474
return;
@@ -166,10 +86,12 @@ export function applyCircleLayoutToClasses(
16686
const angleStep = (2 * Math.PI) / classes.length;
16787

16888
classes.forEach((classModel, index) => {
169-
const classLayout = boxLayoutMap.get(classModel.id);
170-
if (!classLayout) {
171-
return;
172-
}
89+
const classLayout = new BoxLayout();
90+
91+
classLayout.width = CLASS_FOOTPRINT;
92+
classLayout.depth = CLASS_FOOTPRINT;
93+
classLayout.height = CLASS_FOOTPRINT;
94+
classLayout.positionY = classLayout.height / 2.0; // Place directly on foundation
17395

17496
// As Label and regular margin can differ, we offset by half the label margin difference
17597
const zMarginOffset = -APP_LABEL_MARGIN / 2 + APP_MARGIN / 2;
@@ -192,6 +114,8 @@ export function applyCircleLayoutToClasses(
192114

193115
classLayout.positionX = classX;
194116
classLayout.positionZ = classZ;
117+
118+
boxLayoutMap.set(classModel.id, classLayout);
195119
});
196120
});
197121
}

src/utils/elk-layouter.ts

Lines changed: 35 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ import generateUuidv4 from 'explorviz-frontend/src/utils/helpers/uuid4-generator
88
import { metricMappingMultipliers } from 'explorviz-frontend/src/utils/settings/default-settings';
99
import { SelectedClassMetric } from 'explorviz-frontend/src/utils/settings/settings-schemas';
1010
import BoxLayout from 'explorviz-frontend/src/view-objects/layout-models/box-layout';
11-
import {
12-
applyCircleLayoutToClasses,
13-
collectApplicationClassesForCircleLayout,
14-
} from './circle-layouter';
11+
import { applyCircleLayoutToClasses } from './circle-layouter';
1512
import {
1613
applySpiralLayoutToClasses,
1714
calculateSpiralSideLength,
@@ -129,40 +126,33 @@ export default async function layoutLandscape(
129126
applications.forEach((app) => {
130127
const classCount = getAllClassIdsInApplication(app).length;
131128
if (useCustomClassLayout) {
129+
let appSideLength = 1;
132130
if (useCircleLayout) {
133131
const circumference =
134132
classCount * (CLASS_FOOTPRINT * 2 + CLASS_MARGIN * 2);
135-
let diameter;
136133
if (classCount <= 2) {
137-
diameter = CLASS_FOOTPRINT * 4;
134+
appSideLength = CLASS_FOOTPRINT * 4;
138135
} else {
139-
diameter = circumference / Math.PI;
136+
appSideLength = circumference / Math.PI;
140137
}
141-
landscapeGraph.children.push(
142-
createdFixedSizeApplication(app, {
143-
width: diameter,
144-
depth: diameter,
145-
})
146-
);
147138
} else {
148-
// Spiral layout: calculate size based on spiral pattern
149139
const { visualizationSettings: vs } = useUserSettingsStore.getState();
150140

151-
const spiralSideLength = calculateSpiralSideLength(
141+
appSideLength = calculateSpiralSideLength(
152142
classCount,
153143
CLASS_FOOTPRINT,
154144
CLASS_MARGIN,
155145
vs.spiralGap.value,
156146
vs.spiralCenterOffset.value
157147
);
158-
159-
landscapeGraph.children.push(
160-
createdFixedSizeApplication(app, {
161-
width: spiralSideLength,
162-
depth: spiralSideLength,
163-
})
164-
);
165148
}
149+
landscapeGraph.children.push(
150+
createdFixedSizeApplication(app, {
151+
width: appSideLength,
152+
depth: appSideLength,
153+
})
154+
);
155+
// Layout without special class layout algorithm
166156
} else {
167157
landscapeGraph.children.push(
168158
createApplicationGraph(app, removedComponentIds)
@@ -187,17 +177,9 @@ export default async function layoutLandscape(
187177

188178
// Apply the selected layout algorithm to classes
189179
if (useCircleLayout) {
190-
applyCircleLayoutToClasses(
191-
boxLayoutMap,
192-
applications,
193-
removedComponentIds
194-
);
180+
applyCircleLayoutToClasses(boxLayoutMap, applications);
195181
} else if (useSpiralLayout) {
196-
applySpiralLayoutToClasses(
197-
boxLayoutMap,
198-
applications,
199-
removedComponentIds
200-
);
182+
applySpiralLayoutToClasses(boxLayoutMap, applications);
201183
}
202184
}
203185

@@ -315,7 +297,6 @@ function createdFixedSizeApplication(
315297
application: Application,
316298
size: { width: number; depth: number }
317299
) {
318-
// TODO: The fixed sizing does not work correctly
319300
const appGraph = {
320301
id: APP_PREFIX + application.id,
321302
width: size.width,
@@ -332,8 +313,6 @@ function createdFixedSizeApplication(
332313
},
333314
};
334315

335-
populateAppGraph(appGraph, application, new Set());
336-
337316
return appGraph;
338317
}
339318

@@ -342,45 +321,28 @@ function populateAppGraph(
342321
application: Application,
343322
removedComponentIds: Set<string>
344323
) {
345-
// Check if custom class layout is enabled
346-
const { visualizationSettings } = useUserSettingsStore.getState();
347-
const classLayoutAlgorithm = visualizationSettings.classLayoutAlgorithm.value;
348-
const useCustomClassLayout =
349-
classLayoutAlgorithm === 'circle' || classLayoutAlgorithm === 'spiral';
350-
351-
if (useCustomClassLayout) {
352-
const allClassNodes = collectApplicationClassesForCircleLayout(
353-
application,
354-
removedComponentIds
355-
);
324+
application.packages.forEach((component) => {
325+
if (removedComponentIds.has(component.id)) {
326+
return;
327+
}
328+
const packageGraph = {
329+
id: PACKAGE_PREFIX + component.id,
330+
children: [],
331+
layoutOptions: {
332+
algorithm: PACKAGE_ALGORITHM,
333+
aspectRatio: ASPECT_RATIO,
334+
'spacing.nodeNode': CLASS_MARGIN,
335+
'elk.padding': getPaddingForLabelPlacement(
336+
COMPONENT_LABEL_PLACEMENT,
337+
PACKAGE_LABEL_MARGIN,
338+
PACKAGE_MARGIN
339+
),
340+
},
341+
};
342+
appGraph.children.push(packageGraph);
356343

357-
// Add all classes directly to the application graph
358-
appGraph.children.push(...allClassNodes);
359-
} else {
360-
// Normal layout: use packages
361-
application.packages.forEach((component) => {
362-
if (removedComponentIds.has(component.id)) {
363-
return;
364-
}
365-
const packageGraph = {
366-
id: PACKAGE_PREFIX + component.id,
367-
children: [],
368-
layoutOptions: {
369-
algorithm: PACKAGE_ALGORITHM,
370-
aspectRatio: ASPECT_RATIO,
371-
'spacing.nodeNode': CLASS_MARGIN,
372-
'elk.padding': getPaddingForLabelPlacement(
373-
COMPONENT_LABEL_PLACEMENT,
374-
PACKAGE_LABEL_MARGIN,
375-
PACKAGE_MARGIN
376-
),
377-
},
378-
};
379-
appGraph.children.push(packageGraph);
380-
381-
populatePackage(packageGraph.children, component, removedComponentIds);
382-
});
383-
}
344+
populatePackage(packageGraph.children, component, removedComponentIds);
345+
});
384346
}
385347

386348
function populatePackage(

src/utils/spiral-layouter.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ export function calculateSpiralSideLength(
9292
*/
9393
export function applySpiralLayoutToClasses(
9494
boxLayoutMap: Map<string, BoxLayout>,
95-
applications: Application[],
96-
removedComponentIds: Set<string>
95+
applications: Application[]
9796
) {
9897
// Get settings from the store
9998
const { visualizationSettings: vs } = useUserSettingsStore.getState();
@@ -105,20 +104,16 @@ export function applySpiralLayoutToClasses(
105104
const SPIRAL_GAP = vs.spiralGap.value;
106105

107106
applications.forEach((application) => {
108-
if (removedComponentIds.has(application.id)) {
109-
return;
110-
}
111-
112107
// Get application layout to determine spiral size
113108
const appLayout = boxLayoutMap.get(application.id);
114109
if (!appLayout) {
115110
return;
116111
}
117112

118113
// Get all classes in this application
119-
const classes = getAllClassesInApplication(application)
120-
.filter((classModel) => !removedComponentIds.has(classModel.id))
121-
.sort((classA, classB) => classA.fqn!.localeCompare(classB.fqn!));
114+
const classes = getAllClassesInApplication(application).sort(
115+
(classA, classB) => classA.fqn!.localeCompare(classB.fqn!)
116+
);
122117

123118
if (classes.length === 0) {
124119
return;
@@ -177,10 +172,12 @@ export function applySpiralLayoutToClasses(
177172
);
178173

179174
classes.forEach((classModel, _) => {
180-
const classLayout = boxLayoutMap.get(classModel.id);
181-
if (!classLayout) {
182-
return;
183-
}
175+
const classLayout = new BoxLayout();
176+
177+
classLayout.width = CLASS_FOOTPRINT;
178+
classLayout.depth = CLASS_FOOTPRINT;
179+
classLayout.height = CLASS_FOOTPRINT;
180+
classLayout.positionY = classLayout.height / 2.0; // Place directly on foundation
184181

185182
// Calculate position based on current grid coordinates
186183
const classX = centerX + spiralState.x * spacing - CLASS_FOOTPRINT / 2;
@@ -190,6 +187,8 @@ export function applySpiralLayoutToClasses(
190187
classLayout.positionZ = classZ;
191188

192189
spiralState = advanceInSpiral(spiralState, spiralConfig);
190+
191+
boxLayoutMap.set(classModel.id, classLayout);
193192
});
194193
});
195194
}

0 commit comments

Comments
 (0)