Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/app/services/util/position.transformation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,101 @@ export class PositionTransformationService {

this.updateRendering();
}

callRobustAutomaticNodeLayouting() {
console.log("Running Spring Layout…");

const nodes = this.nodeService.getNodes();
const edges: Array<{source: any; target: any}> = [];

// Build edge list from ports
nodes.forEach((n) => {
n.getPorts().forEach((p) => {
const opp = p.getOppositeNode(n.getId());
if (opp) {
edges.push({source: n, target: opp});
}
});
});

// Spring layout parameters
const ITERATIONS = 200;
const SPRING_LENGTH = 150;
const SPRING_STRENGTH = 0.01;
const REPULSION = 50000;
const DAMPING = 0.85;

// Initialize velocities (FIX: number keys instead of string)
const velocity = new Map<number, {x: number; y: number}>();
nodes.forEach((n) => velocity.set(n.getId(), {x: 0, y: 0}));

for (let iter = 0; iter < ITERATIONS; iter++) {
// Forces per node
const forces = new Map<number, {x: number; y: number}>();
nodes.forEach((n) => forces.set(n.getId(), {x: 0, y: 0}));

// --- REPULSION (Coulomb) ---
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i];
const b = nodes[j];

const dx = a.getPositionX() - b.getPositionX();
const dy = a.getPositionY() - b.getPositionY();
const distSq = dx * dx + dy * dy || 0.01;
const force = REPULSION / distSq;

const fx = (dx / Math.sqrt(distSq)) * force;
const fy = (dy / Math.sqrt(distSq)) * force;

forces.get(a.getId())!.x += fx;
forces.get(a.getId())!.y += fy;
forces.get(b.getId())!.x -= fx;
forces.get(b.getId())!.y -= fy;
}
}

// --- SPRINGS (Hooke) ---
edges.forEach((e) => {
const a = e.source;
const b = e.target;

const dx = b.getPositionX() - a.getPositionX();
const dy = b.getPositionY() - a.getPositionY();
const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;

const displacement = dist - SPRING_LENGTH;
const force = SPRING_STRENGTH * displacement;

const fx = (dx / dist) * force;
const fy = (dy / dist) * force;

forces.get(a.getId())!.x += fx;
forces.get(a.getId())!.y += fy;
forces.get(b.getId())!.x -= fx;
forces.get(b.getId())!.y -= fy;
});

// --- UPDATE POSITIONS ---
nodes.forEach((n) => {
const f = forces.get(n.getId())!;
const v = velocity.get(n.getId())!;

// Apply damping
v.x = (v.x + f.x) * DAMPING;
v.y = (v.y + f.y) * DAMPING;

n.setPosition(n.getPositionX() + v.x, n.getPositionY() + v.y);
});
}

console.log("Spring Layout finished.");

// Trigger rendering updates
this.nodeService.nodesUpdated();
this.nodeService.transitionsUpdated();
this.nodeService.connectionsUpdated();
this.trainrunSectionService.trainrunSectionsUpdated();
this.updateRendering();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ <h2 class="SummaryTitle">{{ "app.view.editor-edit-tools-view-component.edit" | t
</sbb-radio-group>
</sbb-expansion-panel>

<sbb-expansion-panel>
<sbb-expansion-panel-header>
<label>
{{ "app.view.editor-edit-tools-view-component.node-auto-layouting-title" | translate }}
</label>
</sbb-expansion-panel-header>
<br />
<button
(click)="onAutomaticNodeLayout()"
[title]="
'app.view.editor-edit-tools-view-component.call-robust-auto-layout-algorithm' | translate
"
class="TrainrunDialog EditorToolButton"
>
<sbb-icon svgIcon="checkpoints-small"></sbb-icon>
</button>
{{ "app.view.editor-edit-tools-view-component.call-robust-auto-layout-algorithm" | translate }}
</sbb-expansion-panel>

<sbb-expansion-panel *ngIf="getAreMultiObjectSelected()" [expanded]="getAreMultiObjectSelected()">
<sbb-expansion-panel-header>
<label style="color: #ec0000">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ export class EditorEditToolsViewComponent implements OnDestroy {
this.uiInteractionService.setActiveOrderingAlgorithm(event.value);
}

onAutomaticNodeLayout() {
console.log(
"onAutomaticNodeLayout: call automatic node layouting --- Implement your callback logic here ---",
);
this.positionTransformationService.callRobustAutomaticNodeLayouting();
}

onAlignElementsLeft() {
this.positionTransformationService.alignSelectedElementsToLeftBorder();
}
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@
"add-netzgrafik-as-copy": "Netzgrafik als Kopie einfügen",
"merge-netzgrafik-tooltip": "Netzgrafik zusammenführen (Zugfahrten, Knoten, Kommentare)",
"merge-netzgrafik": "Netzgrafik durch zusammenführen",
"node-auto-layouting-title": "Automatische Knotenanordnung",
"call-robust-auto-layout-algorithm": "Automatische Knotenanordnung durchführen",
"align-elements-netzgrafik-title": "Elemente ausrichten",
"align-elements-left": "Linksbündig ausrichten",
"align-elements-right": "Rechtsbündig ausrichten",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@
"add-netzgrafik-as-copy": "Ajouter un réticulaire en tant que copie",
"merge-netzgrafik-tooltip": "Fusionner les réticulaires (trajets de train, noeuds, notes)",
"merge-netzgrafik": "Fusionner un réticulaire",
"node-auto-layouting-titel": "Automatique de la disposition des noeuds",
"align-elements-netzgrafik-title": "Aligner plusieurs objets (noeuds)",
"align-elements-left": "Aligner à gauche",
"align-elements-right": "Aligner à droite",
Expand Down