Several transitions between the same pair of states: make transition onto a state that is already connected in that direction adds another transition, which gets its own name right away (Transition 2, Transition 3, skipping names that already leave the state), so two unnamed transitions between one pair never exist; renaming is refused when another transition between the same pair in the same direction already carries the name, the empty name included, while equal names to other targets are only warned about as before; the behavior graph keeps Already connected; self transitions may repeat too, each further loop drawn as a ring around the previous one with the preview showing the next ring, and transition labels that would cover another one slide along their own edge until they are free (pure function freeLabelSpot); test hook transitionLabelAt also returns width and height, e2e 26e6 (#162)
game-maker / gate (push) Successful in 1m58s
game-maker / suite (push) Successful in 6m50s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 16:30:52 +02:00
1 parent 010b6a7f6c
commit 25ec7879a0
9 files changed
+402 -34

No files matched your search

+18 -3
View File
@@ -7,6 +7,7 @@ import {
portsCompatible,
} from '../graph/Graph';
import { isTerminalType, resolveDef, terminalNode } from '../graph/nested';
import { nextTransitionName, parallelTransitions } from '../graph/stateMachine';
import { nodeDef } from '../graph/NodeRegistry';
import { graphLookup } from '../model/graphLookup';
import { newId } from '../ids';
@@ -193,11 +194,17 @@ export class ConnectCommand implements Command {
if (fromNode === toNode && fromDef?.transitionEdges !== true)
throw new CommandError('A node cannot connect to itself');
const out = fromDef?.outputs.find((p) => p.key === this.edge.from.port);
const inp = resolveDef(toNode, graph, lookup)?.inputs.find((p) => p.key === this.edge.to.port);
const toDef = resolveDef(toNode, graph, lookup);
const inp = toDef?.inputs.find((p) => p.key === this.edge.to.port);
if (!out || !inp) throw new CommandError('Port not found');
if (!portsCompatible(out.type, inp.type))
throw new CommandError(`Cannot connect ${out.type} to ${inp.type}`);
if (graph.edges.some((e) => sameEnds(e, this.edge))) throw new CommandError('Already connected');
if (graph.edges.some((e) => sameEnds(e, this.edge))) {
// Transitions are told apart by name, so a further one between the same states gets its own.
if (fromDef?.transitionEdges !== true || toDef?.transitionEdges !== true)
throw new CommandError('Already connected');
this.edge.label = nextTransitionName(graph, this.edge);
}
const occupied = inp.type === 'exec' ? [] : edgesInto(graph, this.edge.to.node, this.edge.to.port);
this.replaced = removeWhere(graph.edges, (e) => occupied.includes(e));
graph.edges.push(this.edge);
@@ -252,8 +259,16 @@ export class NameTransitionCommand implements Command {
execute(project: Project): void {
const edge = requireEdge(project, this.assetId, this.edgeId);
const name = this.name.trim();
// Between the same two states the name is all that tells transitions apart.
if (parallelTransitions(requireGraph(project, this.assetId), edge).some((e) => (e.label ?? '') === name))
throw new CommandError(
name
? `Another transition between these states is already called "${name}"`
: 'Another transition between these states is already unnamed',
);
this.previous = edge.label ?? '';
this.apply(edge, this.name.trim());
this.apply(edge, name);
}
undo(project: Project): void {