diff --git a/packages/merman/src/state/diagram.test.ts b/packages/merman/src/state/diagram.test.ts index 1d73b1901308..a061a11c3e3e 100644 --- a/packages/merman/src/state/diagram.test.ts +++ b/packages/merman/src/state/diagram.test.ts @@ -1383,4 +1383,26 @@ stateDiagram-v2 } } }) + + test("renders transitions between nested composite states sharing horizontal rows without looping", () => { + const source = `stateDiagram-v2 +direction TB +A --> B +A --> C +state D { + state E { + B --> F + } + B --> C +}` + + const output = renderStateDiagram(source) + expect(output).toContain("A") + expect(output).toContain("B") + expect(output).toContain("C") + expect(output).toContain("D") + expect(output).toContain("E") + expect(output).toContain("F") + }) }) + diff --git a/packages/merman/src/state/routing.test.ts b/packages/merman/src/state/routing.test.ts index 3dc9a1f2d0e6..08b546d731b2 100644 --- a/packages/merman/src/state/routing.test.ts +++ b/packages/merman/src/state/routing.test.ts @@ -690,4 +690,30 @@ describe("reconverging vertical elbows", () => { expect(entering.label!.y).toBeLessThan(enteringHorizontalY) expect(leaving.label!.y).toBeLessThan(leavingHorizontalY) }) + + test("routes side-parallel transitions between states sharing the same vertical row without infinite allocation", () => { + const diagram: StateVisibleDiagram = { + direction: "TB", + states: [ + { id: "A", label: "A", kind: "state", parentId: "P1" }, + { id: "B", label: "B", kind: "state", parentId: "P2" }, + ], + transitions: [{ from: "A", to: "B", label: "" }], + composites: [ + { id: "P1", label: "P1" }, + { id: "P2", label: "P2" }, + ], + notes: [], + } + const placements = new Map([ + ["A", bounds("A", 4, 8)], + ["B", bounds("B", 22, 8)], + ]) + + const plans = createStateTransitionRenderPlans(diagram, placements, 29) + expect(plans).toHaveLength(1) + expect(plans[0]!.cells.length).toBeGreaterThan(0) + expect(plans[0]!.cells.length).toBeLessThan(100) + }) }) + diff --git a/packages/merman/src/state/routing.ts b/packages/merman/src/state/routing.ts index 04612445e5d8..a04f153222dd 100644 --- a/packages/merman/src/state/routing.ts +++ b/packages/merman/src/state/routing.ts @@ -327,6 +327,7 @@ function sideParallelTargetApproach( spatialPathClaim(`side-target:${transition.from}:${transition.to}`, "side-target", "route", points) if ( + from.centerY !== to.centerY && space.isFree( claim([ { x: railX, y: to.centerY }, @@ -486,6 +487,17 @@ export function createStateTransitionRoutePlans( const fromParent = statesById.get(transition.from)?.parentId const toParent = statesById.get(transition.to)?.parentId if (fromParent && toParent && fromParent !== toParent) { + if (from.centerY === to.centerY) { + const railY = allocateBottomRail() + return [ + { + ...base, + kind: "bottom-parallel", + railY, + approachX: bottomApproachX(diagram, transition, from, to, bounds, railY), + }, + ] + } return [sideParallel()] } if (verticalCorridorCrossesUnrelatedState(diagram, transition, from, to, bounds)) { @@ -783,12 +795,14 @@ function addSideParallelTransition(builder: StateTransitionRenderBuilder): void const startX = from.left + from.width const startY = from.centerY const endY = targetApproach === "top" ? to.top - 2 : targetApproach === "bottom" ? to.top + to.height + 1 : to.centerY - const verticalStep: 1 | -1 = startY <= endY ? 1 : -1 addRightDeparture(builder, from) addHorizontalLine(builder, startX, railX - 1, startY, 1) - addCell(builder, { x: railX, y: startY, char: verticalStep === 1 ? "╮" : "╯" }) - for (let y = startY + verticalStep; y !== endY; y += verticalStep) addCell(builder, { x: railX, y, char: "│" }) - addCell(builder, { x: railX, y: endY, char: verticalStep === 1 ? "╯" : "╮" }) + if (startY !== endY) { + const verticalStep: 1 | -1 = startY <= endY ? 1 : -1 + addCell(builder, { x: railX, y: startY, char: verticalStep === 1 ? "╮" : "╯" }) + for (let y = startY + verticalStep; y !== endY; y += verticalStep) addCell(builder, { x: railX, y, char: "│" }) + addCell(builder, { x: railX, y: endY, char: verticalStep === 1 ? "╯" : "╮" }) + } if (targetApproach) { const targetX = innerConnectorX(to, from.centerX) for (let x = railX - 1; x > targetX; x--) addCell(builder, { x, y: endY, char: "─" })