Skip to content

fix: lay out microflows written without @position so they can be read - #1155

Merged
ako merged 6 commits into
mendixlabs:mainfrom
tgolembiewski:feature/1154-microflow-auto-layout
Sep 22, 2026
Merged

ako merged 6 commits into
mendixlabs:mainfrom
tgolembiewski:feature/1154-microflow-auto-layout

Conversation

@tgolembiewski

Copy link
Copy Markdown
Contributor

Closes #1154

What

The default layout of a microflow written without @position. Geometry and connection sides only: no MDL syntax change, no change to what a flow does, and a statement carrying @position is never moved.

Reproduce with mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl (three flows, any blank app), then describe microflow:

Flow v0.23.0 this branch
MF_LayoutLong, 40 activities one row, x 360 → 6600 three rows of 18, x 360 → 3080; the joining flow runs bottom → top between the rows
MF_LayoutGuards, three guards activity after the first split at x=730, 370 px from it at x=530, in the branch's column; flow ends at 1540 instead of 1790
MF_LayoutCases, 7 cases cases 5–7 carry @anchor(to: top) sides grouped top / right / bottom (2-3-2), every line arrives on the left

Also: a note is placed above its element's top edge instead of 100 px above its centre (inside any loop box), with the connector bottom → top; a merge stands 40 px after its branch instead of 120; loop boxes get the same gap as activities.

Commits

One concern each; each builds and passes go test ./mdl/executor/.

  1. notes above the element, connector bottom → top
  2. spacing: the main line does not wait for a guard's branch (layout_lanes.go), merge gap, returning branch measured with its end event, run measured by what it occupies
  3. row wrapping (layout_rows.go); rows are separated by measuring what was built, not by estimating
  4. enum case lines grouped by side, and describe reading the case order from pair rank, then the branch's Y
  5. docs: mxcli syntax microflow.layout, write-microflows reference, docs-site, CHANGELOG, the bug-test script, one finding

For the reviewer

  • The anchor pair on an enum case flow stores CASE order (0e69243). Commit 4 keeps that: the table already ranks top/right/bottom-to-left as 0/1/2, so there is one reading path. A model with one pair per case reads as before; a split of up to three cases is written as before.
  • One-time rewrite. A flow with notes or a 4+ case split written by an earlier mxcli stores the old sides, so its first describe → exec with this build writes it once and the second is Unchanged. With the script above: model written by v0.23.0, re-executed with this branch — MF_LayoutLong and MF_LayoutGuards Unchanged, MF_LayoutCases Replaced then Unchanged. describe text is identical before and after.
  • Deliberate trade: with 4+ cases whose branches were re-ordered on the canvas with @position, describe lists them top to bottom rather than as typed. Pinned by TestHandPlacedCaseBranchesAreReadInCanvasOrder.
  • maxEnumSplitBranches stays at 16; lifting it is a separate change.

Testing

  • make build, make test, make lint-go, make check-mdl, make check-findings, make check-wiki-pages, scripts/check-tunnel-deps.sh, scripts/check-skill-mdl.sh (skills, docs-site), mdbook build docs-site, govulncheck: pass.
  • make test-integration with mx 11.12.1 on macOS: passes. (It needs a short TMPDIR there; with the default one mx create-project fails with PathTooLongException and the roundtrip tests report ok without having created a project.)
  • Each new test was checked to fail with its fix reverted.
  • Mendix 11.12.1, MPR v2: mx check 0 errors after executing the script; describe → exec of its three flows reports Unchanged.

tgolembiewski and others added 5 commits September 22, 2026 00:47
A note was offset 100px from its target's CENTRE. That is above an activity, and
inside any box taller than 200px: a note on a loop was drawn on top of the loop's own
body. The connector was written with connection indices 0/0, so it was attached to
the top of the note rather than the side facing the element.

The note now goes above the target's top edge (unchanged for an activity: same y as
before), and the connector runs from the note's bottom to the element's top. The
describer shares defaultAnnotationGeometry, so `position:` is still omitted exactly
when it matches what a rewrite re-derives.

A model written by an earlier mxcli stores the old connector sides, so the first
re-exec of a flow that has notes writes it once; the second reports Unchanged.

Part of mendixlabs#1154.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a merge

Horizontal space the layout spent on nothing (MF_LayoutGuards in
mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl shows the first):

- A guard - `if X then ...; return; end if` - draws its branch in the lane BELOW the
  main line and ends it there, yet the next element was placed past the branch's far
  end: 370px from the split, where two activities stand 40px apart. The main line
  now resumes in the column the branch starts in, and the lane records how far it is
  taken (layout_lanes.go). Only an element that reaches down into that lane -
  another decision, a loop box, an activity with an error handler - waits for it.
- mergeX added HorizontalSpacing/2 to a branch width that is already edge to edge:
  120px before every merge. It is now the same 40px as between activities.
- With the merge that close, a branch ending in RETURN has to be measured with the
  end event it draws (measureBranch), or the merge lands on it.
- HorizontalSpacing is a centre-to-centre pitch, and measureStatements added it on
  top of both widths: two activities measured 400 wide and occupy 280, so every IF
  sized from that put its merge a whole activity too far right per statement.
  gapBetween tightens only the pairs whose arithmetic is exact and keeps the generous
  gap elsewhere - a merge short of its branch is worse than one long.
- A loop box keeps the same 40px from its neighbours as an activity; it used to touch
  them because the cursor treated the box as activity-wide.

The measurer mirrors the lanes: a run's width is max(main line, lane), not a sum.

Part of mendixlabs#1154.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The builder lays the happy path left to right and never stops: every top-level
statement advances posX, nothing moves down. A flow of 40 activities is one row from
x=360 to x=6600 - four screens of sideways scrolling for something 60px tall
(MF_LayoutLong in mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl).

Past MaxRowWidth (2880px, two Studio Pro canvases) the next top-level statement now
starts a new row.

- A row's depth is not known when it begins: a loop box is fitted AFTER its body
  (250 estimated against 310 built) and a note adds its own height. So wrapRow only
  takes a first position and separateRows measures the finished rows once and moves
  them apart to RowGap. An earlier version estimated the height instead; a control
  experiment showed its test passing with the estimate removed, so it is gone.
- The flow joining two rows leaves the BOTTOM of the last element and arrives on
  TOP of the first, so it runs through the band between the rows. Right-to-left it
  was drawn back across the row it left; bottom-to-left still came in over the first
  activities of the new row. An element with a note above it keeps the left side.
- RowGap is 90: Studio Pro prints the output variable and its type under an
  activity, and at 40 that text touched the row below.
- A tail that finishes within RowOverhang stays on its row rather than becoming a
  three-element row underneath. A RETURN never starts a row.
- A statement with @position is never moved (ADR-0008); the row re-anchors to it.
- Only the whole-microflow builder wraps; loop bodies and branches do not.
- Whether a tail fits is asked of the measurer, which is why this follows the commit
  that makes it measure a run by what it occupies.

Part of mendixlabs#1154.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The anchor pair on an enum case flow is not geometry: 0e69243 made it the storage
for CASE order, one distinct pair per case, because stored flow order does not
survive serialization. Past the third pair the table runs out of sides a drawing
would choose - case 4 leaves the split's LEFT corner, cases 5-8 arrive on TOP of
their activity, 9+ on its far side - so a 7-case split is a tangle of lines crossing
each other and the activities between them.

For four or more cases the lines now leave in three groups: the upper third from the
split's top corner, the middle from its right, the lower from its bottom, all
arriving on the left, and mirrored onto the closing merge. Branches are stacked top
to bottom in case order, so no two lines cross. The side is checked against where
the branch actually stands, so a branch moved with @position never gets a line that
leaves away from it.

DESCRIBE reads the order as the pair's rank, then the branch's Y. The table already
ranks top/right/bottom-to-left as 0/1/2, so this is one reading path: a model written
with one pair per case never reaches the tie-break and reads exactly as before, and a
split of up to three cases is written exactly as before.

Deliberate trade, pinned by TestHandPlacedCaseBranchesAreReadInCanvasOrder: with 4+
cases whose branches a user has re-ordered on the canvas with @position, DESCRIBE
lists them top to bottom rather than as typed. CASE branches have no run-time order,
and describe -> exec remains a fixed point.

maxEnumSplitBranches stays at 16 (the table's length) although grouped lines no
longer need the table past its third entry; lifting the limit is a separate change.

A flow with a 4+ case split written by an earlier mxcli is rewritten once on re-exec.

Part of mendixlabs#1154.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `mxcli syntax microflow.layout`, the write-microflows skill (reference/control-flow.md) and the docs-site
  page say what happens when @position is left out, and why a few hand-placed
  statements in an otherwise automatic flow are what produces overlaps.
- mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl: three flows to open
  in Studio Pro (a 40-activity line, three guards, a 7-case split).
- One finding: nothing automatic sees this class, so measure; an anchor that looks
  like layout may be storage; do not estimate what can be measured afterwards.

Part of mendixlabs#1154.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

AI Code Review

Review Summary

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

None found.

What Looks Good

  • Atomic commits: Each commit addresses one specific layout concern (notes, spacing, row wrapping, enum splits, documentation), making the PR easy to review and verify.
  • Comprehensive testing:
    • Added new test files for all modified layout components (layout_rows_test.go, layout_lanes_test.go, cmd_microflows_builder_enum_split_layout_test.go, etc.)
    • Updated existing tests to reflect new layout behavior (layout_span_test.go, microflow_annotation_sharing_test.go, etc.)
    • Added bug test script (mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl) that reproduces the issue and validates the fix
    • All executor tests pass as stated in the PR description
  • Correctness of changes:
    • Row wrapping logic (layout_rows.go) correctly measures actual rendered height instead of estimating, preventing row overlap
    • Lane handling (layout_lanes.go) fixes main line not waiting for guard branches and adjusts merge/returning branch spacing
    • Enum split layout (cmd_microflows_builder_enum_split_layout_test.go) groups cases by side (top/right/bottom) and reads order from pair rank then branch Y
    • Note placement (cmd_microflows_builder_annotations_go) fixed to appear above element's top edge (not center) for tall elements like loop boxes
    • DESCRIBE output updated to reflect new layout while remaining round-trippable
  • Documentation:
    • Updated mxcli syntax microflow.layout documentation
    • CHANGELOG entry added
    • Write-microflows reference updated
    • Bug test script included
  • Code quality:
    • Changes follow existing patterns and conventions
    • No duplication with existing implementations
    • Method receivers and interfaces used correctly
    • No silent side effects or security issues introduced

Recommendation

Approve the PR. The changes are well-tested, correctly implement the layout improvements for microflows without @position, and maintain full backward compatibility through the one-time rewrite mechanism described in the PR. All checklist items are satisfied.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

ako added a commit to tgolembiewski/mxcli that referenced this pull request Sep 22, 2026
02c's five flows predate named `merge` / `join`, so nothing in the file
exercises a backward jump, an error handler rejoining the main flow, or
crossed branches — the shapes a nested IF cannot spell, and the ones with
no coverage when the layout algorithm changes.

Pattern 6 (CX_SYNC_FactoryTelemetry) covers all of them in one flow:
37 elements, 4 merges, 11 joins — a retry whose handler jumps BACKWARD to
a merge above the call, three error handlers joining forward, an inner and
an outer split whose FALSE arms land on the same merge, a fall-through
handler, and a funnel where three paths end.

Measured on a Mendix 11.14.0 app: mendixlabs#1155 lays it out at
2995x540 against 4790x350 on current main — same 37 elements, flow
identical apart from geometry.

It is also a repro for two gaps left in mendixlabs#923, both of which reproduce on
the pre-mendixlabs#1155 build: a one-inbound merge that `exec` writes and the next
`exec` deletes, and a flow that goes from describing faithfully (0
warnings) to "must not be re-executed" (2 warnings) after a single
describe -> exec. Evidence in mendixlabs#923.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

AI Code Review

Let me review this PR against the checklist. The PR is about fixing microflow layout for flows written without @position.

First, I'll check the PR description and commits to understand what's being changed.

The PR body says:

  • "The default layout of a microflow written without @position. Geometry and connection sides only: no MDL syntax change, no change to what a flow does, and a statement carrying @position is never moved."
  • It includes before/after examples showing improvements to layout: row wrapping, spacing, etc.
  • There are 5 commits:
    1. notes above the element, connector bottom → top
    2. spacing: the main line does not wait for a guard's branch, merge gap, returning branch measured with its end event, run measured by what it occupies
    3. row wrapping (layout_rows.go); rows are separated by measuring what was built, not by estimating
    4. enum case lines grouped by side, and describe reading the case order from pair rank, then the branch's Y
    5. docs: mxcli syntax microflow.layout, write-microflows reference, docs-site, CHANGELOG, the bug-test script, one finding

Now let me go through the review checklist:

Overlap & duplication

  • Check proposals list above for existing proposals covering the same functionality
  • Check if the diff introduces code that duplicates existing implementations (look for similar function names, command names, types)
    • Looking at the diff, I see new files like layout_rows.go, layout_lanes.go, etc. These seem to be new implementations for the layout logic.
  • Check doctype test files above for existing test coverage of the feature area
    • The PR adds a bug-test: mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl
    • I don't see existing doctype tests for this specific layout feature in the provided list, but there are microflow-examples.mdl
  • Flag if the PR re-documents already-shipped features as new
    • The PR is about fixing layout for microflows without @position, which seems to be a bug fix, not documenting existing features as new.

MDL syntax design (for new or modified MDL syntax)

The PR states: "no MDL syntax change" - so this section should not apply. Let me verify:

  • Looking at the diff, I don't see any changes to grammar files (MDLParser.g4 or MDLLexer.g4)
  • The changes are all in the executor/layout logic, not in MDL syntax
  • The PR explicitly says "no MDL syntax change"
  • So this section should be skipped as there are no MDL syntax changes

Full-stack consistency (for MDL features)

Since there are no MDL syntax changes (just bug fixes to existing layout logic), this section doesn't apply. The PR is fixing how existing MDL is interpreted/layoutted, not adding new MDL features.

Test coverage

  • New packages should have test files
    • I see new test files: layout_rows_test.go, layout_lanes_test.go, layout_helpers_test.go, cmd_microflows_builder_enum_split_layout_test.go, cmd_microflows_builder_annotations_geometry_test.go
  • New executor commands should have MDL examples in mdl-examples/doctype-tests/
    • This is a bug fix, not a new command. The PR adds a bug-test: mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl
  • Integration paths should be tested, not just helpers
    • The PR mentions: "Each new test was checked to fail with its fix reverted." and "make test-integration with mx 11.12.1 on macOS: passes."
    • Also: "Mendix 11.12.1, MPR v2: mx check 0 errors after executing the script; describe → exec of its three flows reports Unchanged."
  • No time.Sleep for synchronization — use channels or polling with timeout
    • I don't see any time.Sleep in the diff that was shown

Security & robustness

  • Command injection, XSS, SQL injection, temp file issues
    • This is layout logic, not dealing with user input in a way that would cause injection
  • Unix sockets should use restrictive permissions (0600)
    • Not relevant to this PR
  • File I/O should not be in hot paths (event loops, per-keystroke handlers)
    • This is layout logic during microflow building, not in hot paths
  • No silent side effects on typos (e.g., auto-creating resources on misspelled names)
    • Not relevant to layout logic
  • Correct method receivers (pointer vs value) for mutations
    • I need to check if method receivers are correct

Scope & atomicity

  • Each commit should do one thing — a feature, a bugfix, or a refactor, not a mix
    • The PR has 5 commits, each described as addressing one concern:
      1. notes above the element, connector bottom → top
      2. spacing: the main line does not wait for a guard's branch, merge gap, returning branch measured with its end event, run measured by what it occupies
      3. row wrapping (layout_rows.go); rows are separated by measuring what was built, not by estimating
      4. enum case lines grouped by side, and describe reading the case order from pair rank, then the branch's Y
      5. docs: mxcli syntax microflow.layout, write-microflows reference, docs-site, CHANGELOG, the bug-test script, one finding
    • Each commit seems to address one specific aspect
  • PR should be scoped to a single feature or concern — flag if description needs "and" between unrelated items
    • The PR description is focused on "fix: lay out microflows written without @position so they can be read"
    • All commits relate to improving the layout of microflows without @position
  • Independent features should be in separate PRs
    • This seems to be a single feature (improving layout for microflows without @position)
  • Refactors touching many files should be their own commit
    • The PR has multiple commits, each touching a focused set of files

Code quality

  • Refactors should be applied consistently (look for old patterns still present)
    • I need to check if old patterns are still present
  • Manually maintained lists (keyword lists, type mappings) should be flagged as maintenance risks
    • I don't see any manually maintained lists being modified
  • Design docs should match the actual implementation
    • The PR updates docs: "docs-site/src/language/microflow-structure.md" and CHANGELOG.md

Bugs & correctness

  • Logic errors, race conditions, resource leaks
    • This is layout logic, so race conditions are unlikely
  • Error handling gaps
    • Need to check if error handling is proper

Now let me look at specific code changes to verify correctness:

From the diff, I can see:

  1. In layout_rows.go:

    • Implements row wrapping logic for long microflows
    • Uses MaxRowWidth = 2880 (two canvas widths)
    • Has logic to wrap rows based on actual measured width, not estimates
    • Includes tests for wrapping behavior
  2. In layout_lanes.go:

    • Implements lane logic for guards (if..return; end if)
    • Main line no longer waits for guard's branch
    • Has logic for when the lane is busy
  3. In layout.go (measurement logic):

    • Changed how width is calculated (removed extra HorizontalSpacing)
    • Changed measureBranch to include end event for RETURN statements
    • Fixed gapBetween calculation
    • Updated measureIfStatement, measureEnumSplitStatement, etc.
  4. In cmd_microflows_builder_control.go:

    • Fixed merge position calculation
    • Added guard logic (no merge for guards)
    • Added lowerLane tracking
  5. In cmd_microflows_builder_annotations.go:

    • Fixed note placement to be above element's top edge (not center)
    • Added objectHeight function to get actual height of elements
  6. In cmd_microflows_builder_annotations_geometry_test.go:

    • Test for note placement on tall elements
  7. In cmd_microflows_builder_actions.go (enum split layout):

    • Changed how enum split cases are grouped (top/right/bottom)
    • Added logic to read case order from pair rank and branch Y position
    • Added tests for enum split layout
  8. In cmd_microflows_show_helpers.go:

    • Updated lines function to include targetHeight for note placement
  9. In mdl/backend/modelsdk/microflow_write.go:

    • Fixed annotation flow connection indexes (notes now go from bottom to top)
  10. Added bug test: mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl

    • Contains three test microflows: MF_LayoutLong, MF_LayoutGuards, MF_LayoutCases
    • Demonstrates the before/after behavior

Let me check if there are any issues:

  1. In layout_rows.go:

    • The wrapRow function seems correct
    • shouldWrap logic looks reasonable
    • takeWrapAnchors function: I need to verify the anchor logic
      • It says: "It leaves the BOTTOM of its origin and arrives on TOP of its destination"
      • In the function: if flow.OriginConnectionIndex != AnchorBottom || flow.DestinationConnectionIndex != AnchorTop
      • This seems correct for a bottom-to-top connection
  2. In layout_lanes.go:

    • guardBranchInset = SplitWidth + HorizontalSpacing/2 - ActivityWidth/2
    • guardMainWidth = SplitWidth
    • laneGap = HorizontalSpacing - ActivityWidth
    • These constants seem reasonable based on the comments
  3. In layout.go:

    • The measureBranch function now properly accounts for the end event when lastStmtIsReturn
    • gapBetween function:
      • For IfStmt: toNextCentre = HorizontalSpacing / 2
      • For EnumSplit/InheritanceSplit: returns HorizontalSpacing
      • For others: toNextCentre = HorizontalSpacing - ActivityWidth/2
      • leftHalf calculation looks correct
      • Returns max(toNextCentre-leftHalf, 20) - the 20px minimum seems reasonable
    • measureEnumSplitStatement:
      • Changed from: width := SplitWidth + HorizontalSpacing/2 + maxBranchWidth + HorizontalSpacing/2 + MergeSize
      • To: width := SplitWidth + HorizontalSpacing/2 + maxBranchWidth + MergeSize
      • This removes one HorizontalSpacing, which matches the gapBetween fix
    • measureIfStatement: Similar change, plus special handling for guards
  4. In cmd_microflows_builder_control.go:

    • addIfStatement:
      • mergeX calculation: splitX + SplitWidth + HorizontalSpacing/2 + branchWidth
      • For guards: fb.posX = thenStartX (resumes right after split)
      • Added fb.reserveLowerLane for guards
    • addLoopStatement/addWhileStatement:
      • Fixed posX calculation: loopLeftX + loopWidth + ActivityWidth/2 + (HorizontalSpacing - ActivityWidth)
      • Which simplifies to: loopLeftX + loopWidth + HorizontalSpacing - ActivityWidth/2
      • This seems correct for placing the next element after the loop box
  5. In cmd_microflows_builder_annotations.go:

    • defaultAnnotationGeometry:
      • Now takes targetHeight parameter
      • Calculates: above := targetHeight/2 - ActivityHeight/2
      • So note is placed above the top edge of the target element
    • objectHeight: returns actual height if available, else ActivityHeight
  6. In cmd_microflows_builder_enum_split_layout_test.go:

    • Tests for enum split layout grouping (top/right/bottom)
    • Tests for reading case order from grouped lines
    • Test for hand-placed branches being read in canvas order (not input order)
  7. In cmd_microflows_show_helpers.go:

    • orderedEnumSplitFlows now uses branchY (from activityMap) as tie-breaker after splitCaseOrder
    • This implements reading case order from pair rank, then branch Y
  8. In mdl/backend/modelsdk/microflow_write.go:

    • annotationFlowToGen:
      • Changed OriginConnectionIndex from 0 to 2 (bottom)
      • DestinationConnectionIndex remains 0 (top)
      • Comment explains: "A note sits ABOVE the element it documents, so its line leaves the note's bottom edge and enters the element's top."

Now let me check against the checklist items more specifically:

Overlap & duplication

  • I don't see obvious duplication in the code changes. The layout logic was previously in a single file and has been split into focused modules (layout_rows.go, layout_lanes.go, etc.), which seems like a good refactor.
  • The bug-test file is new and specific to this fix.

MDL syntax design

  • No MDL syntax changes, as stated in the PR.

Full-stack consistency

  • Not applicable since no new MDL syntax is added.

Test coverage

  • New test files are present for the new layout logic
  • The bug-test file is added
  • Integration testing was done (make test-integration passes)
  • No time.Sleep seen in the diff

Security & robustness

  • Nothing obvious stands out as a security issue
  • Method receivers: I need to spot-check
    • In layout_rows.go: functions are on *flowBuilder (pointer) - correct for mutating state
    • In layout.go: measureStatements is on *layoutMeasurer (pointer) - but it doesn't mutate state, so could be value receiver. However, since it's called from elsewhere and the struct is small, pointer is fine.
    • In layout_lanes.go: reserveLowerLane and clearLowerLane are on *flowBuilder - correct for mutating state
    • In cmd_microflows_builder_annotations.go: objectHeight is a function (not method) - fine
    • In cmd_microflows_builder.go: allowWrap and row are fields being set - correct

Scope & atomicity

  • Each commit addresses one specific aspect as described
  • The PR is scoped to improving layout for microflows without @position
  • No unrelated items mixed in

Code quality

  • Refactors appear consistent: layout logic split into focused files
  • No manually maintained lists being modified
  • Docs updated: microflow-structure.md and CHANGELOG.md

Bugs & correctness

  • The logic seems sound based on the explanations in comments
  • The fix has been verified to fail when reverted (as stated in PR)
  • Integration tests pass
  • The describe → exec roundtrip shows Unchanged after the fix is applied

One thing I notice: in layout_rows.go, the wrapRow function:

func (fb *flowBuilder) wrapRow(noteAbove bool) {
	next := fb.rowBottom() + RowGap + ActivityHeight/2
	fb.posX = fb.row.startX
	fb.posY = next
	fb.baseY = next
	fb.row.firstObject = len(fb.objects)
	fb.row.pendingWrapEdge = true
	fb.row.wrapNoteAbove = noteAbove
	fb.row.starts = append(fb.row.starts, fb.row.firstObject)
}

This sets fb.posY to the bottom of the current row plus RowGap plus ActivityHeight/2.
ActivityHeight/2 would center an activity at that position, which seems correct for starting a new row.

In takeWrapAnchors:

func (fb *flowBuilder) takeWrapAnchors() (origin, destination int, ok bool) {
	// ...
	if flow.OriginConnectionIndex != AnchorBottom || flow.DestinationConnectionIndex != AnchorTop {
		t.Fatalf("wrap edge (%d,%d)->(%d,%d) anchored %d->%d, want bottom(%d)->top(%d)",
			from.X, from.Y, to.X, to.Y,
			flow.OriginConnectionIndex, flow.DestinationConnectionIndex, AnchorBottom, AnchorTop)
	}

This validates that wrap edges go from bottom to top, which matches the comment.

In layout.go, the measureBranch function:

func (m *layoutMeasurer) measureBranch(stmts []ast.MicroflowStatement) Bounds {
	b := m.measureStatements(stmts)
	if !lastStmtIsReturn(stmts) {
		return b
	}
	if b.Width == 0 {
		// A bare RETURN: the end event stands where the first activity would.
		b.Width = ActivityWidth/2 + EventSize/2
	} else {
		b.Width += HorizontalSpacing - ActivityWidth/2 + EventSize/2
	}
	return b
}

This seems correct: for a RETURN at the end of a branch, we need to account for the end event that gets drawn after the last activity.

The gapBetween function:

func gapBetween(prev, next ast.MicroflowStatement) int {
	// Distance from prev's measured right edge to the centre the builder gives next.
	var toNextCentre int
	switch prev.(type) {
	case *ast.IfStmt:
		// After an IF, posX = (measured right edge) + HorizontalSpacing/2.
		toNextCentre = HorizontalSpacing / 2
	case *ast.EnumSplitStmt, *ast.InheritanceSplitStmt:
		return HorizontalSpacing
	default:
		// A simple activity, or a loop box: after either, the builder centres the
		// next element one activity-gap plus half an activity past the right edge.
		// A simple activity: centre-to-centre pitch, less its own right half.
		toNextCentre = HorizontalSpacing - ActivityWidth/2
	}
	// How far next reaches left of the centre it is given.
	var leftHalf int
	switch next.(type) {
	case *ast.IfStmt, *ast.EnumSplitStmt:
		leftHalf = SplitWidth / 2
	default:
		// An activity — and a loop box, whose left edge the builder now puts where
		// an activity's would be.
		leftHalf = ActivityWidth / 2
	}

---
*Automated review via OpenRouter (Nemotron Super 120B) — [workflow source](https://github.com/mendixlabs/mxcli/blob/main/.github/workflows/ai-review.yml)*

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Microflow auto-layout: long flows never wrap, guards waste the main line, 4+ case splits draw crossing lines

2 participants