Skip to content

Why two passes

The engine of the first version of this project (Jqy, 2025) went once from the root down. Each node placed its children, then each child placed its own children. This page shows why that fails, and how ireko fixes it.

A node that places its children must know how much space each child needs. In one pass from the root down, it does not know. The child has not placed its own children yet. The old engine gave each child the space of one box. A child with a large subtree then covered its neighbors.

The first version55 overlapping pairs

ACrootBA1A2A3B1B2B3B4C1C2C3C4

ireko0 overlapping pairs

ireko: the same tree with no overlaps. The rings of A and C get their own cells next to the tree of B.ACrootA1A2A3BB1B2B3B4C1C2C3C4
The default demo of the first version: a root with two rings (A, C) and a tree (B). Both engines use boxes of 110 by 54 and a spacing of 24. The red hatches are the overlapping areas. The audit of the first version found none of them.

The old engine had an audit, but the audit compared each child with its parent only. It did not compare siblings, so it found none of these overlaps.

User interface toolkits had this problem a long time ago. Their solution is the same in each toolkit:

ToolkitFirst passSecond pass
FlutterSizes go upThe parent sets the position
WPFMeasureArrange
SwiftUIsizeThatFitsplaceSubviews
CSS GridTrack sizing from the contentPlacement in the tracks

ireko does the same with two passes over the tree.

  1. Measure, from the leaves up. Each node gets its size after its children have theirs. The strategy of the node arranges the measured children. The engine then knows the extent of the node: the rectangle that holds the node and its whole subtree.
  2. Place, from the root down. The root is at the origin. Each node adds the offsets of its children to its own position. A strategy does not know absolute positions, so a strategy cannot get them wrong.
r {
  a { a1 a2 a3 a4 }
  b
  c radial { c1 c2 c3 c4 c5 }
}
Each column of the root is as wide as the extent of its child. The ring of c needs a wide column, and b needs a narrow one.raa1a2a3a4bcc1c2c3c4c5
Each column of the root is as wide as the extent of its child. The ring of c needs a wide column, and b needs a narrow one.Open in the playground →

A strategy sees the extents, not only the boxes. The grid makes each column as wide as the widest extent in it. The radial strategy puts each subtree in a circle, and keeps the circles apart. The pipeline page shows the two passes step by step.

You can examine this claim yourself. Your browser can lay out 2000 random trees at this time, with the engine of this page. The audit of each layout looks for overlapping boxes and for boxes outside their container.

seed 35
The first version1084 of 2000 trees overlap
ireko, in your browser0 of 0 trees overlap

Each tree has up to four levels, with mixed strategies and modes. The audit of each layout looks for overlaps and for boxes outside their container.

The last random treen1n7n11n12n16n23n0n2n3n4n5n6n8n9n10n13n14n15n17n18n19n20n21n22n24

The property tests of ireko do the same for each change, with all strategies, both modes, all fit policies, random options and both routers. The audit and a second, independent check must find nothing. A longer run with 20000 random inputs found no overlap and no box outside its container.

The two passes visit each node two times. The audit uses a sweep line, so it does not compare each pair of boxes.

TreeFirst versionireko
1111 nodes, grid469 ms6.6 ms
1111 nodes, mixed strategies896 ms8.6 ms
11111 nodes, grid8.1 s58 ms
11111 nodes, mixed strategies9.1 s48 ms

These times include the audit. The measurements are from one machine, so compare the ratio, not the numbers. The review of the project has the method.