colors for choices in .dot

This commit is contained in:
2025-07-13 20:29:58 +02:00
parent 66c98da632
commit 3a7465f16c
2 changed files with 67 additions and 81 deletions

View File

@@ -27,17 +27,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0' implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
// uml model parsing
// implementation 'org.springframework.statemachine:spring-statemachine-uml:3.2.0'
// deps for SCXMLWriter
// implementation "org.apache.commons:scxml4j:1.0.0"
implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0'
// deps for parsing java AST // deps for parsing java AST
implementation 'com.github.javaparser:javaparser-core:3.27.0' implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'

View File

@@ -2,12 +2,14 @@ package click.kamil.springstatemachineexporter.ast.out;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition; import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.HashSet; import java.util.*;
import java.util.List;
import java.util.Set;
public class Dot implements StateMachineExporter { public class Dot implements StateMachineExporter {
private static final String[] CHOICE_COLORS = {
"blue", "darkgreen", "darkred", "darkorange", "darkviolet", "teal", "brown", "crimson"
};
@Override @Override
public String export(List<Transition> transitions, public String export(List<Transition> transitions,
Set<String> startStates, Set<String> startStates,
@@ -21,29 +23,36 @@ public class Dot implements StateMachineExporter {
sb.append(" start [shape=point, label=\"\"];\n"); sb.append(" start [shape=point, label=\"\"];\n");
// Identify choice states and assign colors
Set<String> statesWithChoice = new HashSet<>(); Set<String> statesWithChoice = new HashSet<>();
Map<String, String> choiceColorMap = new HashMap<>();
int colorIndex = 0;
for (Transition t : transitions) { for (Transition t : transitions) {
if ("withChoice".equals(t.getType())) { if ("withChoice".equals(t.getType()) && t.getSourceStates() != null) {
for (String source : t.getSourceStates()) { for (String source : t.getSourceStates()) {
statesWithChoice.add(simplify(source)); String simplified = simplify(source);
statesWithChoice.add(simplified);
if (!choiceColorMap.containsKey(simplified)) {
choiceColorMap.put(simplified, CHOICE_COLORS[colorIndex % CHOICE_COLORS.length]);
colorIndex++;
}
} }
} }
} }
// Collect all states
Set<String> allStates = new HashSet<>(); Set<String> allStates = new HashSet<>();
for (Transition t : transitions) { for (Transition t : transitions) {
if (t.getSourceStates() != null) { if (t.getSourceStates() != null) {
for (String s : t.getSourceStates()) { for (String s : t.getSourceStates()) allStates.add(simplify(s));
allStates.add(simplify(s));
}
} }
if (t.getTargetStates() != null) { if (t.getTargetStates() != null) {
for (String tgt : t.getTargetStates()) { for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt));
allStates.add(simplify(tgt));
}
} }
} }
// Declare normal states
for (String state : allStates) { for (String state : allStates) {
if (!statesWithChoice.contains(state + "_choice")) { if (!statesWithChoice.contains(state + "_choice")) {
String shape = endStates.contains(state) ? "doublecircle" : "circle"; String shape = endStates.contains(state) ? "doublecircle" : "circle";
@@ -52,27 +61,29 @@ public class Dot implements StateMachineExporter {
} }
} }
// Declare choice pseudostates
for (String state : statesWithChoice) { for (String state : statesWithChoice) {
String choiceNode = state + "_choice"; sb.append(" ").append(state).append("_choice")
sb.append(" ").append(choiceNode)
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n"); .append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
} }
sb.append("\n"); sb.append("\n");
// Start state arrows
for (String start : startStates) { for (String start : startStates) {
sb.append(" start -> ").append(simplify(start)).append(";\n"); sb.append(" start -> ").append(simplify(start)).append(";\n");
} }
sb.append("\n"); sb.append("\n");
// Connect normal states to their choice pseudostates
for (String state : statesWithChoice) { for (String state : statesWithChoice) {
String choiceNode = state + "_choice"; sb.append(" ").append(state).append(" -> ").append(state).append("_choice;\n");
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
} }
sb.append("\n"); sb.append("\n");
// Transitions
for (Transition t : transitions) { for (Transition t : transitions) {
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue; if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
@@ -98,99 +109,84 @@ public class Dot implements StateMachineExporter {
edgeSource = source + "_choice"; edgeSource = source + "_choice";
} }
// Guard // Label: event [guard] / actions
String guardText = ""; StringBuilder label = new StringBuilder();
if (t.getGuard() != null && !t.getGuard().isBlank()) {
if (useLambdaGuards && t.isLambdaGuard()) { if (t.getEvent() != null && !t.getEvent().isBlank()) {
guardText = "λ"; label.append(escapeDotString(simplify(t.getEvent())));
} else {
guardText = escapeDotString(
t.getGuard().replaceAll("[\\n\\r]+", " ")
.replaceAll("\\s{2,}", " ")
.trim()
);
} }
// Guard
if (t.getGuard() != null && !t.getGuard().isBlank()) {
if (!label.isEmpty()) label.append(" ");
String guardText = useLambdaGuards && t.isLambdaGuard() ? "λ"
: escapeDotString(t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim());
label.append("[").append(guardText).append("]");
} }
// Actions // Actions
String actionsText = "";
if (t.getActions() != null && !t.getActions().isEmpty()) { if (t.getActions() != null && !t.getActions().isEmpty()) {
StringBuilder actionBuilder = new StringBuilder(); StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.getActions().size(); i++) { for (int i = 0; i < t.getActions().size(); i++) {
if (i > 0) actionBuilder.append(", "); if (i > 0) actionBuilder.append(", ");
String action = t.getActions().get(i); String action = t.getActions().get(i);
boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i); boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i);
if (useLambdaGuards && isLambda) { actionBuilder.append(useLambdaGuards && isLambda ? "λ" : escapeDotString(action));
actionBuilder.append("λ");
} else {
actionBuilder.append(escapeDotString(action));
} }
}
actionsText = actionBuilder.toString();
}
// Event and label
StringBuilder label = new StringBuilder();
if (t.getEvent() != null && !t.getEvent().isBlank()) {
label.append(escapeDotString(simplify(t.getEvent())));
}
if (!guardText.isBlank()) {
if (!label.isEmpty()) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (!label.isEmpty()) label.append(" / "); if (!label.isEmpty()) label.append(" / ");
label.append(actionsText); label.append(actionBuilder);
} }
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) { // Order
if ((t.getType().equals("withChoice") || t.getType().equals("withJunction")) && t.getOrder() != null) {
if (!label.isEmpty()) label.append(" "); if (!label.isEmpty()) label.append(" ");
label.append("(order=").append(t.getOrder()).append(")"); label.append("(order=").append(t.getOrder()).append(")");
} }
// Build edge attributes // Edge attributes
StringBuilder edgeAttributes = new StringBuilder(); StringBuilder edgeAttrs = new StringBuilder();
if (!label.isEmpty()) { if (!label.isEmpty()) {
appendEdgeAttr(edgeAttributes, "label", label.toString()); appendEdgeAttr(edgeAttrs, "label", label.toString());
} }
// Style/color by type
switch (t.getType()) { switch (t.getType()) {
case "withInternal": case "withInternal":
appendEdgeAttr(edgeAttributes, "style", "dashed"); appendEdgeAttr(edgeAttrs, "style", "dashed");
appendEdgeAttr(edgeAttributes, "color", "gray"); appendEdgeAttr(edgeAttrs, "color", "gray");
break; break;
case "withLocal": case "withLocal":
appendEdgeAttr(edgeAttributes, "style", "dotted"); appendEdgeAttr(edgeAttrs, "style", "dotted");
appendEdgeAttr(edgeAttributes, "color", "darkgray"); appendEdgeAttr(edgeAttrs, "color", "darkgray");
break; break;
case "withChoice": case "withChoice":
appendEdgeAttr(edgeAttributes, "style", "solid"); String choiceColor = choiceColorMap.getOrDefault(source, "blue");
appendEdgeAttr(edgeAttributes, "color", "blue"); appendEdgeAttr(edgeAttrs, "style", "solid");
appendEdgeAttr(edgeAttrs, "color", choiceColor);
break; break;
case "withJunction": case "withJunction":
appendEdgeAttr(edgeAttributes, "style", "dotted"); appendEdgeAttr(edgeAttrs, "style", "dotted");
appendEdgeAttr(edgeAttributes, "color", "purple"); appendEdgeAttr(edgeAttrs, "color", "purple");
break; break;
case "withFork": case "withFork":
appendEdgeAttr(edgeAttributes, "style", "bold"); appendEdgeAttr(edgeAttrs, "style", "bold");
appendEdgeAttr(edgeAttributes, "color", "green"); appendEdgeAttr(edgeAttrs, "color", "green");
break; break;
case "withJoin": case "withJoin":
appendEdgeAttr(edgeAttributes, "style", "bold"); appendEdgeAttr(edgeAttrs, "style", "bold");
appendEdgeAttr(edgeAttributes, "color", "darkorange"); appendEdgeAttr(edgeAttrs, "color", "darkorange");
break; break;
case "withExternal": case "withExternal":
default: default:
appendEdgeAttr(edgeAttributes, "style", "solid"); appendEdgeAttr(edgeAttrs, "style", "solid");
appendEdgeAttr(edgeAttributes, "color", "black"); appendEdgeAttr(edgeAttrs, "color", "black");
break;
} }
String edgeLabel = !edgeAttributes.isEmpty() ? " [" + edgeAttributes + "]" : ""; sb.append(" ").append(edgeSource).append(" -> ").append(target);
if (!edgeAttrs.isEmpty()) {
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n"); sb.append(" [").append(edgeAttrs).append("]");
}
sb.append(";\n");
} }
} }
} }
@@ -206,8 +202,7 @@ public class Dot implements StateMachineExporter {
private String escapeDotString(String input) { private String escapeDotString(String input) {
if (input == null) return ""; if (input == null) return "";
return input.replace("\\", "\\\\") return input.replace("\\", "\\\\").replace("\"", "\\\"");
.replace("\"", "\\\"");
} }
private void appendEdgeAttr(StringBuilder sb, String key, String value) { private void appendEdgeAttr(StringBuilder sb, String key, String value) {