// Package layout partitions terminal screen space into rectangular regions // using a constraint-based solver. // // Under the hood, it relies on the [Cassowary constraint solver algorithm] to // resolve competing size requirements. Each constraint carries a priority, so // the solver can pick the best trade-off when not every requirement fits. // // # How It Works // // A [Layout] takes the available area and a list of constraints ([Len], [Ratio], // [Percent], [Fill], [Min], [Max]) and produces a set of non-overlapping rectangles. // The solver tries to honour every constraint; when that is impossible it // relaxes lower-priority ones first. // // You are not required to use [Layout] at all. If you prefer manual control, // you can compute [uv.Rectangle] values yourself with plain arithmetic. // // # Acknowledgements // // This implementation is heavily based on [Ratatui] source code and // is roughly 1:1 translation from Rust with some minor API adjustments. // // [Cassowary constraint solver algorithm]: https://en.wikipedia.org/wiki/Cassowary_(software) // [Ratatui]: https://ratatui.rs/ package layout import ( "fmt" "hash/fnv" "math" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/ultraviolet/internal/casso" ) // floatPrecisionMultiplier scales cell positions into a higher-precision // floating-point domain before handing them to the constraint solver. // The number of trailing zeros determines the decimal precision kept // during rounding. const floatPrecisionMultiplier float64 = 100.0 const ( required casso.Priority = 1_001_001_000 strong casso.Priority = 1_000_000 medium casso.Priority = 1_000 weak casso.Priority = 1 ) const ( // spacerSizeEq enforces equal sizing across spacers. // // ┌ ┐┌───┐┌ ┐┌───┐┌ ┐ // ==x │ │ ==x │ │ ==x // └ ┘└───┘└ ┘└───┘└ ┘ spacerSizeEq = required / 10.0 // minSizeGTE enforces the lower-bound inequality for [Min] constraints. // // ┌────────┐ // │Min(>=x)│ // └────────┘ minSizeGTE = strong * 100.0 // maxSizeLTE enforces the upper-bound inequality for [Max] constraints. // // ┌────────┐ // │Max(<=x)│ // └────────┘ maxSizeLTE = strong * 100.0 // lengthSizeEq pins the segment to the exact size requested by a [Len] constraint. // // ┌────────┐ // │Len(==x)│ // └────────┘ lengthSizeEq = strong * 10.0 // percentSizeEq tries to make the segment match its [Percent] target. // // ┌────────────┐ // │Percent(==x)│ // └────────────┘ percentSizeEq = strong // ratioSizeEq tries to make the segment match its [Ratio] target. // // ┌────────────┐ // │Ratio(==x,y)│ // └────────────┘ ratioSizeEq = strong / 10.0 // minSizeEq is an equality companion for the [Min] lower-bound; it // nudges the segment toward the minimum value when room is tight. // // ┌────────┐ // │Min(==x)│ // └────────┘ minSizeEq = medium * 10.0 // maxSizeEq is an equality companion for the [Max] upper-bound; // it nudges the segment toward the maximum value. // // ┌────────┐ // │Max(==x)│ // └────────┘ maxSizeEq = medium * 10.0 // fillGrow lets [Fill] segments expand into available space. // // ┌─────────────────────┐ // │<= Fill(x) =>│ // └─────────────────────┘ fillGrow = medium // grow is a general expansion priority (used by [Min] in non-legacy flex). // // ┌────────────┐ // │<= Min(x) =>│ // └────────────┘ grow casso.Priority = 100.0 // spaceGrow allows spacers to expand and absorb remaining room. // // ┌ ┐ // <= x => // └ ┘ spaceGrow = weak * 10.0 // allSegmentGrow encourages all segments to share the same size. // // ┌───────┐ // │<= x =>│ // └───────┘ allSegmentGrow = weak ) // Splitted holds the rectangles produced by a [Layout.Split] call. type Splitted []uv.Rectangle // Assign stores each resulting rectangle into the corresponding pointer. // // Nil pointers are silently skipped. // // Panics when len(areas) exceeds the number of rectangles in [Splitted]. // // # Examples // // var top, bottom uv.Rectangle // // layout.New(layout.Fill(1), layout.Len(1)). // Split(area). // Assign(&top, &bottom) func (s Splitted) Assign(areas ...*uv.Rectangle) { for i := range areas { if areas[i] != nil { *areas[i] = s[i] } } } // Direction controls whether a [Layout] arranges its segments // horizontally (left to right) or vertically (top to bottom). type Direction int const ( // DirectionVertical - layout segments are arranged top to bottom (default). DirectionVertical Direction = iota // DirectionHorizontal - layout segments are arranged side by side (left to right). DirectionHorizontal ) // New returns a [Layout] configured with the given direction and constraints. func New(direction Direction, constraints ...Constraint) Layout { return Layout{ Direction: direction, Constraints: constraints, } } // Vertical is shorthand for New(DirectionVertical, constraints...). func Vertical(constraints ...Constraint) Layout { return New(DirectionVertical, constraints...) } // Horizontal is shorthand for New(DirectionHorizontal, constraints...). func Horizontal(constraints ...Constraint) Layout { return New(DirectionHorizontal, constraints...) } // Layout splits a rectangular area into smaller rectangles using a set of constraints. // It is the primary building block for structuring terminal user interfaces. // // Fields: // - Direction: whether segments flow vertically or horizontally. // - Constraints: the sizing rules ([Len], [Ratio], [Percent], [Fill], [Min], [Max]). // - Padding: inset applied to the outer area before solving. // - Flex: strategy for distributing leftover space among segments. // - Spacing: gap (or overlap, if negative) between adjacent segments. // // Internally, sizes are resolved by a Cassowary linear-constraint solver that // satisfies as many rules as it can, preferring higher-priority constraints // when trade-offs are necessary. type Layout struct { Direction Direction Constraints []Constraint Padding Padding // Spacing is the gap between adjacent segments, measured in cells. // A negative value causes segments to overlap by that many cells. Spacing int Flex Flex } // WithDirection returns a shallow copy of the layout using the specified direction. func (l Layout) WithDirection(direction Direction) Layout { l.Direction = direction return l } // WithPadding returns a shallow copy of the layout using the specified padding. func (l Layout) WithPadding(padding Padding) Layout { l.Padding = padding return l } // WithFlex returns a shallow copy of the layout using the specified flex strategy. func (l Layout) WithFlex(flex Flex) Layout { l.Flex = flex return l } // WithSpacing returns a shallow copy of the layout using the specified spacing value. func (l Layout) WithSpacing(spacing int) Layout { l.Spacing = spacing return l } // WithConstraints returns a shallow copy of the layout with the given // constraints appended to its existing list. func (l Layout) WithConstraints(constraints ...Constraint) Layout { l.Constraints = append(l.Constraints, constraints...) return l } // SplitWithSpacers divides the given area into content segments and the // gaps (spacers) between them. It returns both slices; use [Layout.Split] // if you only need the content rectangles. func (l Layout) SplitWithSpacers(area uv.Rectangle) (segments, spacers Splitted) { segments, spacers, err := l.splitCached(area) if err != nil { panic(err) } return segments, spacers } // Split partitions the area into content rectangles according to the // layout's direction and constraints. // // Because every constraint is evaluated against the total area, mixing // relative constraints (Percent, Ratio) with absolute ones (Min, Max, Len) // can produce ambiguous results. For example, splitting 100 cells as // [Min(20), Percent(50), Percent(50)] will not necessarily yield [20, 40, 40]. func (l Layout) Split(area uv.Rectangle) Splitted { segments, _ := l.SplitWithSpacers(area) return segments } func (l Layout) splitCached(area uv.Rectangle) (segments, spacers []uv.Rectangle, err error) { globalCacheMu.Lock() defer globalCacheMu.Unlock() key := l.cacheKey(area) if v, ok := globalCache.Get(key); ok { return v.Segments, v.Spacers, nil } segments, spacers, err = l.split(area) if err != nil { return nil, nil, err } globalCache.Add(key, cacheValue{Segments: segments, Spacers: spacers}) return segments, spacers, nil } func (l Layout) split(area uv.Rectangle) (segments, spacers []uv.Rectangle, err error) { s := casso.NewSolver() innerArea := l.Padding.apply(area) var areaStart, areaEnd float64 switch l.Direction { case DirectionHorizontal: areaStart = float64(innerArea.Min.X) * floatPrecisionMultiplier areaEnd = float64(innerArea.Max.X) * floatPrecisionMultiplier case DirectionVertical: areaStart = float64(innerArea.Min.Y) * floatPrecisionMultiplier areaEnd = float64(innerArea.Max.Y) * floatPrecisionMultiplier } // <───────────────────────────────────area_size──────────────────────────────────> // ┌─area_start area_end─┐ // V V // ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐ // │ │ │ │ │ │ │ │ // V V V V V V V V // ┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐ // │ Max(20) │ │ Max(20) │ │ Max(20) │ // └ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘ // ^ ^ ^ ^ ^ ^ ^ ^ // │ │ │ │ │ │ │ │ // └─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┘ // │ ┃ │ ┃ │ ┃ │ // └────────────╂───────────┴────────────╂───────────┴────────────╂──Spacers──┘ // ┃ ┃ ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━Segments━━━━━━━━┛ variableCount := len(l.Constraints)*2 + 2 variables := make([]casso.Symbol, variableCount) for i := range variableCount { variables[i] = casso.New() } spacerElements := newElements(variables) segmentElements := newElements(variables[1:]) spacing := l.Spacing areaEl := element{ start: variables[0], end: variables[len(variables)-1], } if err := configureArea(s, areaEl, areaStart, areaEnd); err != nil { return nil, nil, fmt.Errorf("configure area: %w", err) } if err := configureVariableInAreaConstraints(s, variables, areaEl); err != nil { return nil, nil, fmt.Errorf("configure variable in area constraints: %w", err) } if err := configureVariableConstraints(s, variables); err != nil { return nil, nil, fmt.Errorf("configure variable constraints: %w", err) } if err := configureFlexConstraints(s, areaEl, spacerElements, l.Flex, spacing); err != nil { return nil, nil, fmt.Errorf("configure flex constraints: %w", err) } if err := configureConstraints(s, areaEl, segmentElements, l.Constraints, l.Flex); err != nil { return nil, nil, fmt.Errorf("configure constraints: %w", err) } if err := configureFillConstraints(s, segmentElements, l.Constraints, l.Flex); err != nil { return nil, nil, fmt.Errorf("configure fill constraints: %w", err) } if l.Flex != FlexLegacy { for i := 0; i < len(segmentElements)-1; i++ { left := segmentElements[i] right := segmentElements[i+1] if _, err := s.Add(allSegmentGrow, left.sizeEqSize(right)); err != nil { return nil, nil, fmt.Errorf("add has size constraint: %w", err) } } } changes := make(map[casso.Symbol]float64, variableCount) for _, v := range variables { changes[v] = s.Val(v) } segments = changesToRects(changes, segmentElements, innerArea, l.Direction) spacers = changesToRects(changes, spacerElements, innerArea, l.Direction) return segments, spacers, nil } func (l Layout) cacheKey(area uv.Rectangle) cacheKey { h := fnv.New64a() for _, c := range l.Constraints { c.hash(h) } return cacheKey{ Area: area, Direction: l.Direction, ConstraintsHash: h.Sum64(), Padding: l.Padding, Spacing: l.Spacing, Flex: l.Flex, } } func changesToRects( changes map[casso.Symbol]float64, elements []element, area uv.Rectangle, direction Direction, ) []uv.Rectangle { var rects []uv.Rectangle for _, e := range elements { startVal := changes[e.start] endVal := changes[e.end] startRounded := int(math.Round(math.Round(startVal) / floatPrecisionMultiplier)) endRounded := int(math.Round(math.Round(endVal) / floatPrecisionMultiplier)) size := max(0, endRounded-startRounded) switch direction { case DirectionHorizontal: rect := uv.Rect(startRounded, area.Min.Y, size, area.Dy()) rects = append(rects, rect) case DirectionVertical: rect := uv.Rect(area.Min.X, startRounded, area.Dx(), size) rects = append(rects, rect) } } return rects } // configureFillConstraints ensures that every [Fill] (and, outside legacy mode, // every [Min]) segment grows proportionally to its scaling factor, so that // remaining space is shared according to the declared weights. // // [Fill(1), Fill(1)] // ┌──────┐┌──────┐ // │abcdef││abcdef│ // └──────┘└──────┘ // // [Fill(1), Fill(2)] // ┌──────┐┌────────────┐ // │abcdef││abcdefabcdef│ // └──────┘└────────────┘ // // size == base_element * scaling_factor func configureFillConstraints( s *casso.Solver, segments []element, constraints []Constraint, flex Flex, ) error { var ( validConstraints []Constraint validSegments []element ) for i := 0; i < min(len(constraints), len(segments)); i++ { c := constraints[i] seg := segments[i] switch c.(type) { case Fill, Min: if _, ok := c.(Min); ok && flex == FlexLegacy { continue } validConstraints = append(validConstraints, c) validSegments = append(validSegments, seg) } } for _, indices := range combinations(len(validConstraints), 2) { i, j := indices[0], indices[1] leftConstraint := validConstraints[i] leftSegment := validSegments[i] rightConstraint := validConstraints[j] rightSegment := validSegments[j] getScalingFactor := func(c Constraint) float64 { var scalingFactor float64 switch c := c.(type) { case Fill: scale := float64(c) scalingFactor = 1e-6 scalingFactor = max(scalingFactor, scale) case Min: scalingFactor = 1 } return scalingFactor } leftScalingFactor := getScalingFactor(leftConstraint) rightScalingFactor := getScalingFactor(rightConstraint) c := casso.NewConstraint(casso.EQ, 0, leftSegment.end.T(rightScalingFactor), leftSegment.start.T(-rightScalingFactor), rightSegment.end.T(-leftScalingFactor), rightSegment.start.T(leftScalingFactor), ) if _, err := s.Add(grow, c); err != nil { return fmt.Errorf("add constraint: %w", err) } } return nil } func configureConstraints( s *casso.Solver, area element, segments []element, constraints []Constraint, flex Flex, ) error { for i := 0; i < min(len(constraints), len(segments)); i++ { constraint := constraints[i] segment := segments[i] switch constraint := constraint.(type) { case Max: size := int(constraint) if _, err := s.Add(maxSizeLTE, segment.sizeLTE(size)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(maxSizeEq, segment.sizeEqConst(size)); err != nil { return fmt.Errorf("add constraints: %w", err) } case Min: size := int(constraint) if _, err := s.Add(minSizeGTE, segment.sizeGTE(size)); err != nil { return fmt.Errorf("add has min size constraint: %w", err) } if flex == FlexLegacy { if _, err := s.Add(minSizeEq, segment.sizeEqConst(size)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } else { if _, err := s.Add(fillGrow, segment.sizeEqSize(area)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } case Len: length := int(constraint) if _, err := s.Add(lengthSizeEq, segment.sizeEqConst(length)); err != nil { return fmt.Errorf("add has int size constraint: %w", err) } case Percent: f := float64(constraint) / 100 if _, err := s.Add(percentSizeEq, segment.sizeEqScaledSize(area, f)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } case Ratio: f := float64(constraint.Num) / float64(max(1, constraint.Den)) if _, err := s.Add(ratioSizeEq, segment.sizeEqScaledSize(area, f)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } case Fill: if _, err := s.Add(fillGrow, segment.sizeEqSize(area)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } } return nil } func configureFlexConstraints( s *casso.Solver, area element, spacers []element, flex Flex, spacing int, ) error { var spacersExceptFirstAndLast []element if len(spacers) > 2 { spacersExceptFirstAndLast = spacers[1 : len(spacers)-1] } switch flex { case FlexLegacy: for _, sp := range spacersExceptFirstAndLast { if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } if len(spacers) >= 2 { first, last := spacers[0], spacers[len(spacers)-1] if _, err := s.Add(required-weak, first.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(required-weak, last.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } } case FlexSpaceEvenly: for _, indices := range combinations(len(spacers), 2) { i, j := indices[0], indices[1] left, right := spacers[i], spacers[j] if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } for _, sp := range spacers { if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } } case FlexSpaceAround: if len(spacers) <= 2 { for _, indices := range combinations(len(spacers), 2) { i, j := indices[0], indices[1] left, right := spacers[i], spacers[j] if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } for _, sp := range spacers { if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } } } else { first, rest := spacers[0], spacers[1:] last, middle := rest[len(rest)-1], rest[:len(rest)-1] for _, indices := range combinations(len(middle), 2) { i, j := indices[0], indices[1] left, right := middle[i], middle[j] if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } if len(middle) > 0 { firstMiddle := middle[0] for _, e := range []element{first, last} { if _, err := s.Add(spacerSizeEq, firstMiddle.sizeEqDouble(e)); err != nil { return fmt.Errorf("add has double size constraint: %w", err) } } } for _, sp := range spacers { if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil { return fmt.Errorf("add has min size constraint: %w", err) } if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } } case FlexSpaceBetween: for _, indices := range combinations(len(spacersExceptFirstAndLast), 2) { i, j := indices[0], indices[1] left, right := spacersExceptFirstAndLast[i], spacersExceptFirstAndLast[j] if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } for _, sp := range spacersExceptFirstAndLast { if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } } if len(spacers) >= 2 { first, last := spacers[0], spacers[len(spacers)-1] if _, err := s.Add(required-weak, first.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(required-weak, last.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } } case FlexStart: for _, sp := range spacersExceptFirstAndLast { if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } if len(spacers) >= 2 { first := spacers[0] last := spacers[len(spacers)-1] if _, err := s.Add(required-weak, first.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(grow, last.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } } case FlexCenter: for _, sp := range spacersExceptFirstAndLast { if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } if len(spacers) >= 2 { first, last := spacers[0], spacers[len(spacers)-1] if _, err := s.Add(grow, first.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(grow, last.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(spacerSizeEq, first.sizeEqSize(last)); err != nil { return fmt.Errorf("add constraints: %w", err) } } case FlexEnd: for _, sp := range spacersExceptFirstAndLast { if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil { return fmt.Errorf("add has size constraint: %w", err) } } if len(spacers) >= 2 { first := spacers[0] last := spacers[len(spacers)-1] if _, err := s.Add(required-weak, last.empty()); err != nil { return fmt.Errorf("add constraints: %w", err) } if _, err := s.Add(grow, first.sizeEqSize(area)); err != nil { return fmt.Errorf("add constraints: %w", err) } } } return nil } func configureVariableConstraints( s *casso.Solver, variables []casso.Symbol, ) error { // ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐ // │ │ │ │ │ │ │ │ // v v v v v v v v // ┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐ // │ Max(20) │ │ Max(20) │ │ Max(20) │ // └ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘ // ^ ^ ^ ^ ^ ^ ^ ^ // └v0 └v1 └v2 └v3 └v4 └v5 └v6 └v7 variables = variables[1:] count := len(variables) for i := 0; i < count-count%2; i += 2 { left, right := variables[i], variables[i+1] if _, err := s.Add(required, casso.NewConstraint(casso.LTE, 0, left.T(1), right.T(-1))); err != nil { return fmt.Errorf("add constraint: %w", err) } } return nil } func configureVariableInAreaConstraints( s *casso.Solver, variables []casso.Symbol, area element, ) error { for _, v := range variables { if _, err := s.Add(required, casso.NewConstraint(casso.GTE, 0, v.T(1), area.start.T(-1))); err != nil { return fmt.Errorf("add start constraint: %w", err) } if _, err := s.Add(required, casso.NewConstraint(casso.LTE, 0, v.T(1), area.end.T(-1))); err != nil { return fmt.Errorf("add end constraint: %w", err) } } return nil } func configureArea( s *casso.Solver, area element, areaStart, areaEnd float64, ) error { if _, err := s.Add(required, casso.NewConstraint(casso.EQ, -areaStart, area.start.T(1))); err != nil { return fmt.Errorf("add start constraint: %w", err) } if _, err := s.Add(required, casso.NewConstraint(casso.EQ, -areaEnd, area.end.T(1))); err != nil { return fmt.Errorf("add end constraint: %w", err) } return nil } func newElements(variables []casso.Symbol) []element { count := len(variables) elements := make([]element, 0, count/2+1) for i := 0; i < count-count%2; i += 2 { s, e := variables[i], variables[i+1] elements = append(elements, element{start: s, end: e}) } return elements } type element struct { start, end casso.Symbol } func (e element) empty() casso.Constraint { return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1)) } func (e element) sizeEqConst(size int) casso.Constraint { return casso.NewConstraint(casso.EQ, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1)) } func (e element) sizeLTE(size int) casso.Constraint { return casso.NewConstraint(casso.LTE, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1)) } func (e element) sizeGTE(size int) casso.Constraint { return casso.NewConstraint(casso.GTE, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1)) } func (e element) sizeEqSize(other element) casso.Constraint { return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-1), other.start.T(1)) } func (e element) sizeEqScaledSize(other element, f float64) casso.Constraint { return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-f), other.start.T(f)) } func (e element) sizeEqDouble(other element) casso.Constraint { return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-2), other.start.T(2)) } func combinations(n, k int) [][]int { combs := binomial(n, k) data := make([][]int, combs) if len(data) == 0 { return nil } data[0] = make([]int, k) for i := range data[0] { data[0][i] = i } for i := 1; i < combs; i++ { next := make([]int, k) copy(next, data[i-1]) nextCombination(next, n, k) data[i] = next } return data } func nextCombination(s []int, n, k int) { for j := k - 1; j >= 0; j-- { if s[j] == n+j-k { continue } s[j]++ for l := j + 1; l < k; l++ { s[l] = s[j] + l - j } break } } func binomial(n, k int) int { if n < 0 || k < 0 { panic("layout: binomial: negative input") } if n < k { return 0 } // (n,k) = (n, n-k) if k > n/2 { k = n - k } b := 1 for i := 1; i <= k; i++ { b = (n - k + i) * b / i } return b }