okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
f678006
main
1// Package layout partitions terminal screen space into rectangular regions 2// using a constraint-based solver. 3// 4// Under the hood, it relies on the [Cassowary constraint solver algorithm] to 5// resolve competing size requirements. Each constraint carries a priority, so 6// the solver can pick the best trade-off when not every requirement fits. 7// 8// # How It Works 9// 10// A [Layout] takes the available area and a list of constraints ([Len], [Ratio], 11// [Percent], [Fill], [Min], [Max]) and produces a set of non-overlapping rectangles. 12// The solver tries to honour every constraint; when that is impossible it 13// relaxes lower-priority ones first. 14// 15// You are not required to use [Layout] at all. If you prefer manual control, 16// you can compute [uv.Rectangle] values yourself with plain arithmetic. 17// 18// # Acknowledgements 19// 20// This implementation is heavily based on [Ratatui] source code and 21// is roughly 1:1 translation from Rust with some minor API adjustments. 22// 23// [Cassowary constraint solver algorithm]: https://en.wikipedia.org/wiki/Cassowary_(software) 24// [Ratatui]: https://ratatui.rs/ 25package layout 26 27import ( 28"fmt" 29"hash/fnv" 30"math" 31 32 uv"github.com/charmbracelet/ultraviolet" 33"github.com/charmbracelet/ultraviolet/internal/casso" 34) 35 36// floatPrecisionMultiplier scales cell positions into a higher-precision 37// floating-point domain before handing them to the constraint solver. 38// The number of trailing zeros determines the decimal precision kept 39// during rounding. 40const floatPrecisionMultiplier float64 = 100.0 41 42const ( 43required casso.Priority = 1_001_001_000 44strong casso.Priority = 1_000_000 45medium casso.Priority = 1_000 46weak casso.Priority = 1 47) 48 49const ( 50// spacerSizeEq enforces equal sizing across spacers. 51// 52// ┌ ┐┌───┐┌ ┐┌───┐┌ ┐ 53// ==x │ │ ==x │ │ ==x 54// └ ┘└───┘└ ┘└───┘└ ┘ 55spacerSizeEq = required / 10.0 56 57// minSizeGTE enforces the lower-bound inequality for [Min] constraints. 58// 59// ┌────────┐ 60// │Min(>=x)│ 61// └────────┘ 62minSizeGTE = strong * 100.0 63 64// maxSizeLTE enforces the upper-bound inequality for [Max] constraints. 65// 66// ┌────────┐ 67// │Max(<=x)│ 68// └────────┘ 69maxSizeLTE = strong * 100.0 70 71// lengthSizeEq pins the segment to the exact size requested by a [Len] constraint. 72// 73// ┌────────┐ 74// │Len(==x)│ 75// └────────┘ 76lengthSizeEq = strong * 10.0 77 78// percentSizeEq tries to make the segment match its [Percent] target. 79// 80// ┌────────────┐ 81// │Percent(==x)│ 82// └────────────┘ 83percentSizeEq = strong 84 85// ratioSizeEq tries to make the segment match its [Ratio] target. 86// 87// ┌────────────┐ 88// │Ratio(==x,y)│ 89// └────────────┘ 90ratioSizeEq = strong / 10.0 91 92// minSizeEq is an equality companion for the [Min] lower-bound; it 93// nudges the segment toward the minimum value when room is tight. 94// 95// ┌────────┐ 96// │Min(==x)│ 97// └────────┘ 98minSizeEq = medium * 10.0 99 100// maxSizeEq is an equality companion for the [Max] upper-bound; 101// it nudges the segment toward the maximum value. 102// 103// ┌────────┐ 104// │Max(==x)│ 105// └────────┘ 106maxSizeEq = medium * 10.0 107 108// fillGrow lets [Fill] segments expand into available space. 109// 110// ┌─────────────────────┐ 111// │<= Fill(x) =>│ 112// └─────────────────────┘ 113fillGrow = medium 114 115// grow is a general expansion priority (used by [Min] in non-legacy flex). 116// 117// ┌────────────┐ 118// │<= Min(x) =>│ 119// └────────────┘ 120grow casso.Priority = 100.0 121 122// spaceGrow allows spacers to expand and absorb remaining room. 123// 124// ┌ ┐ 125// <= x => 126// └ ┘ 127spaceGrow = weak * 10.0 128 129// allSegmentGrow encourages all segments to share the same size. 130// 131// ┌───────┐ 132// │<= x =>│ 133// └───────┘ 134allSegmentGrow = weak 135) 136 137// Splitted holds the rectangles produced by a [Layout.Split] call. 138type Splitted []uv.Rectangle 139 140// Assign stores each resulting rectangle into the corresponding pointer. 141// 142// Nil pointers are silently skipped. 143// 144// Panics when len(areas) exceeds the number of rectangles in [Splitted]. 145// 146// # Examples 147// 148// var top, bottom uv.Rectangle 149// 150// layout.New(layout.Fill(1), layout.Len(1)). 151// Split(area). 152// Assign(&top, &bottom) 153func (s Splitted )Assign (areas ...* uv.Rectangle ) { 154for i := range areas { 155if areas [i ]!= nil { 156* areas [i ]= s [i ] 157 } 158 } 159} 160 161// Direction controls whether a [Layout] arranges its segments 162// horizontally (left to right) or vertically (top to bottom). 163type Direction int 164 165const ( 166// DirectionVertical - layout segments are arranged top to bottom (default). 167DirectionVertical Direction = iota 168// DirectionHorizontal - layout segments are arranged side by side (left to right). 169DirectionHorizontal 170) 171 172// New returns a [Layout] configured with the given direction and constraints. 173func New (direction Direction ,constraints ... Constraint )Layout { 174return Layout { 175Direction :direction , 176Constraints :constraints , 177 } 178} 179 180// Vertical is shorthand for New(DirectionVertical, constraints...). 181func Vertical (constraints ... Constraint )Layout { 182return New (DirectionVertical ,constraints ... ) 183} 184 185// Horizontal is shorthand for New(DirectionHorizontal, constraints...). 186func Horizontal (constraints ... Constraint )Layout { 187return New (DirectionHorizontal ,constraints ... ) 188} 189 190// Layout splits a rectangular area into smaller rectangles using a set of constraints. 191// It is the primary building block for structuring terminal user interfaces. 192// 193// Fields: 194// - Direction: whether segments flow vertically or horizontally. 195// - Constraints: the sizing rules ([Len], [Ratio], [Percent], [Fill], [Min], [Max]). 196// - Padding: inset applied to the outer area before solving. 197// - Flex: strategy for distributing leftover space among segments. 198// - Spacing: gap (or overlap, if negative) between adjacent segments. 199// 200// Internally, sizes are resolved by a Cassowary linear-constraint solver that 201// satisfies as many rules as it can, preferring higher-priority constraints 202// when trade-offs are necessary. 203type Layout struct { 204Direction Direction 205Constraints []Constraint 206Padding Padding 207// Spacing is the gap between adjacent segments, measured in cells. 208// A negative value causes segments to overlap by that many cells. 209Spacing int 210Flex Flex 211} 212 213// WithDirection returns a shallow copy of the layout using the specified direction. 214func (l Layout )WithDirection (direction Direction )Layout { 215l .Direction = direction 216 217return l 218} 219 220// WithPadding returns a shallow copy of the layout using the specified padding. 221func (l Layout )WithPadding (padding Padding )Layout { 222l .Padding = padding 223return l 224} 225 226// WithFlex returns a shallow copy of the layout using the specified flex strategy. 227func (l Layout )WithFlex (flex Flex )Layout { 228l .Flex = flex 229 230return l 231} 232 233// WithSpacing returns a shallow copy of the layout using the specified spacing value. 234func (l Layout )WithSpacing (spacing int )Layout { 235l .Spacing = spacing 236 237return l 238} 239 240// WithConstraints returns a shallow copy of the layout with the given 241// constraints appended to its existing list. 242func (l Layout )WithConstraints (constraints ... Constraint )Layout { 243l .Constraints = append (l .Constraints ,constraints ... ) 244 245return l 246} 247 248// SplitWithSpacers divides the given area into content segments and the 249// gaps (spacers) between them. It returns both slices; use [Layout.Split] 250// if you only need the content rectangles. 251func (l Layout )SplitWithSpacers (area uv.Rectangle ) (segments ,spacers Splitted ) { 252segments ,spacers ,err := l .splitCached (area ) 253if err != nil { 254panic (err ) 255 } 256 257return segments ,spacers 258} 259 260// Split partitions the area into content rectangles according to the 261// layout's direction and constraints. 262// 263// Because every constraint is evaluated against the total area, mixing 264// relative constraints (Percent, Ratio) with absolute ones (Min, Max, Len) 265// can produce ambiguous results. For example, splitting 100 cells as 266// [Min(20), Percent(50), Percent(50)] will not necessarily yield [20, 40, 40]. 267func (l Layout )Split (area uv.Rectangle )Splitted { 268segments ,_ := l .SplitWithSpacers (area ) 269 270return segments 271} 272 273func (l Layout )splitCached (area uv.Rectangle ) (segments ,spacers []uv.Rectangle ,err error ) { 274globalCacheMu .Lock () 275defer globalCacheMu .Unlock () 276 277key := l .cacheKey (area ) 278 279if v ,ok := globalCache .Get (key );ok { 280return v .Segments ,v .Spacers ,nil 281 } 282 283segments ,spacers ,err = l .split (area ) 284if err != nil { 285return nil ,nil ,err 286 } 287 288globalCache .Add (key ,cacheValue {Segments :segments ,Spacers :spacers }) 289 290return segments ,spacers ,nil 291} 292 293func (l Layout )split (area uv.Rectangle ) (segments ,spacers []uv.Rectangle ,err error ) { 294s := casso .NewSolver () 295 296innerArea := l .Padding .apply (area ) 297 298var areaStart ,areaEnd float64 299 300switch l .Direction { 301case DirectionHorizontal : 302areaStart = float64 (innerArea .Min .X )* floatPrecisionMultiplier 303areaEnd = float64 (innerArea .Max .X )* floatPrecisionMultiplier 304 305case DirectionVertical : 306areaStart = float64 (innerArea .Min .Y )* floatPrecisionMultiplier 307areaEnd = float64 (innerArea .Max .Y )* floatPrecisionMultiplier 308 } 309 310// <───────────────────────────────────area_size──────────────────────────────────> 311// ┌─area_start area_end─┐ 312// V V 313// ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐ 314// │ │ │ │ │ │ │ │ 315// V V V V V V V V 316// ┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐ 317// │ Max(20) │ │ Max(20) │ │ Max(20) │ 318// └ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘ 319// ^ ^ ^ ^ ^ ^ ^ ^ 320// │ │ │ │ │ │ │ │ 321// └─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┘ 322// │ ┃ │ ┃ │ ┃ │ 323// └────────────╂───────────┴────────────╂───────────┴────────────╂──Spacers──┘ 324// ┃ ┃ ┃ 325// ┗━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━Segments━━━━━━━━┛ 326 327variableCount := len (l .Constraints )* 2 + 2 328 329variables := make ([]casso.Symbol ,variableCount ) 330for i := range variableCount { 331variables [i ]= casso .New () 332 } 333 334spacerElements := newElements (variables ) 335segmentElements := newElements (variables [1 :]) 336 337spacing := l .Spacing 338 339areaEl := element { 340start :variables [0 ], 341end :variables [len (variables )- 1 ], 342 } 343 344if err := configureArea (s ,areaEl ,areaStart ,areaEnd );err != nil { 345return nil ,nil ,fmt .Errorf ("configure area: %w" ,err ) 346 } 347 348if err := configureVariableInAreaConstraints (s ,variables ,areaEl );err != nil { 349return nil ,nil ,fmt .Errorf ("configure variable in area constraints: %w" ,err ) 350 } 351 352if err := configureVariableConstraints (s ,variables );err != nil { 353return nil ,nil ,fmt .Errorf ("configure variable constraints: %w" ,err ) 354 } 355 356if err := configureFlexConstraints (s ,areaEl ,spacerElements ,l .Flex ,spacing );err != nil { 357return nil ,nil ,fmt .Errorf ("configure flex constraints: %w" ,err ) 358 } 359 360if err := configureConstraints (s ,areaEl ,segmentElements ,l .Constraints ,l .Flex );err != nil { 361return nil ,nil ,fmt .Errorf ("configure constraints: %w" ,err ) 362 } 363 364if err := configureFillConstraints (s ,segmentElements ,l .Constraints ,l .Flex );err != nil { 365return nil ,nil ,fmt .Errorf ("configure fill constraints: %w" ,err ) 366 } 367 368if l .Flex != FlexLegacy { 369for i := 0 ;i < len (segmentElements )- 1 ;i ++ { 370left := segmentElements [i ] 371right := segmentElements [i + 1 ] 372 373if _ ,err := s .Add (allSegmentGrow ,left .sizeEqSize (right ));err != nil { 374return nil ,nil ,fmt .Errorf ("add has size constraint: %w" ,err ) 375 } 376 } 377 } 378 379changes := make (map [casso.Symbol ]float64 ,variableCount ) 380 381for _ ,v := range variables { 382changes [v ]= s .Val (v ) 383 } 384 385segments = changesToRects (changes ,segmentElements ,innerArea ,l .Direction ) 386spacers = changesToRects (changes ,spacerElements ,innerArea ,l .Direction ) 387 388return segments ,spacers ,nil 389} 390 391func (l Layout )cacheKey (area uv.Rectangle )cacheKey { 392h := fnv .New64a () 393 394for _ ,c := range l .Constraints { 395c .hash (h ) 396 } 397 398return cacheKey { 399Area :area , 400Direction :l .Direction , 401ConstraintsHash :h .Sum64 (), 402Padding :l .Padding , 403Spacing :l .Spacing , 404Flex :l .Flex , 405 } 406} 407 408func changesToRects ( 409changes map [casso.Symbol ]float64 , 410elements []element , 411area uv.Rectangle , 412direction Direction , 413) []uv.Rectangle { 414var rects []uv.Rectangle 415 416for _ ,e := range elements { 417startVal := changes [e .start ] 418endVal := changes [e .end ] 419 420startRounded := int (math .Round (math .Round (startVal )/ floatPrecisionMultiplier )) 421endRounded := int (math .Round (math .Round (endVal )/ floatPrecisionMultiplier )) 422 423size := max (0 ,endRounded - startRounded ) 424 425switch direction { 426case DirectionHorizontal : 427rect := uv .Rect (startRounded ,area .Min .Y ,size ,area .Dy ()) 428 429rects = append (rects ,rect ) 430 431case DirectionVertical : 432rect := uv .Rect (area .Min .X ,startRounded ,area .Dx (),size ) 433 434rects = append (rects ,rect ) 435 } 436 } 437 438return rects 439} 440 441// configureFillConstraints ensures that every [Fill] (and, outside legacy mode, 442// every [Min]) segment grows proportionally to its scaling factor, so that 443// remaining space is shared according to the declared weights. 444// 445// [Fill(1), Fill(1)] 446// ┌──────┐┌──────┐ 447// │abcdef││abcdef│ 448// └──────┘└──────┘ 449// 450// [Fill(1), Fill(2)] 451// ┌──────┐┌────────────┐ 452// │abcdef││abcdefabcdef│ 453// └──────┘└────────────┘ 454// 455// size == base_element * scaling_factor 456func configureFillConstraints ( 457s * casso.Solver , 458segments []element , 459constraints []Constraint , 460flex Flex , 461)error { 462var ( 463validConstraints []Constraint 464validSegments []element 465 ) 466 467for i := 0 ;i < min (len (constraints ),len (segments ));i ++ { 468c := constraints [i ] 469seg := segments [i ] 470 471switch c .(type ) { 472case Fill ,Min : 473if _ ,ok := c .(Min );ok && flex == FlexLegacy { 474continue 475 } 476 477validConstraints = append (validConstraints ,c ) 478validSegments = append (validSegments ,seg ) 479 } 480 } 481 482for _ ,indices := range combinations (len (validConstraints ),2 ) { 483i ,j := indices [0 ],indices [1 ] 484 485leftConstraint := validConstraints [i ] 486leftSegment := validSegments [i ] 487 488rightConstraint := validConstraints [j ] 489rightSegment := validSegments [j ] 490 491getScalingFactor := func (c Constraint )float64 { 492var scalingFactor float64 493 494switch c := c .(type ) { 495case Fill : 496scale := float64 (c ) 497 498scalingFactor = 1e-6 499scalingFactor = max (scalingFactor ,scale ) 500 501case Min : 502scalingFactor = 1 503 } 504 505return scalingFactor 506 } 507 508leftScalingFactor := getScalingFactor (leftConstraint ) 509rightScalingFactor := getScalingFactor (rightConstraint ) 510 511c := casso .NewConstraint (casso .EQ ,0 , 512leftSegment .end .T (rightScalingFactor ), 513leftSegment .start .T (- rightScalingFactor ), 514rightSegment .end .T (- leftScalingFactor ), 515rightSegment .start .T (leftScalingFactor ), 516 ) 517 518if _ ,err := s .Add (grow ,c );err != nil { 519return fmt .Errorf ("add constraint: %w" ,err ) 520 } 521 } 522 523return nil 524} 525 526func configureConstraints ( 527s * casso.Solver , 528area element , 529segments []element , 530constraints []Constraint , 531flex Flex , 532)error { 533for i := 0 ;i < min (len (constraints ),len (segments ));i ++ { 534constraint := constraints [i ] 535segment := segments [i ] 536 537switch constraint := constraint .(type ) { 538case Max : 539size := int (constraint ) 540 541if _ ,err := s .Add (maxSizeLTE ,segment .sizeLTE (size ));err != nil { 542return fmt .Errorf ("add constraints: %w" ,err ) 543 } 544 545if _ ,err := s .Add (maxSizeEq ,segment .sizeEqConst (size ));err != nil { 546return fmt .Errorf ("add constraints: %w" ,err ) 547 } 548 549case Min : 550size := int (constraint ) 551 552if _ ,err := s .Add (minSizeGTE ,segment .sizeGTE (size ));err != nil { 553return fmt .Errorf ("add has min size constraint: %w" ,err ) 554 } 555 556if flex == FlexLegacy { 557if _ ,err := s .Add (minSizeEq ,segment .sizeEqConst (size ));err != nil { 558return fmt .Errorf ("add has size constraint: %w" ,err ) 559 } 560 }else { 561if _ ,err := s .Add (fillGrow ,segment .sizeEqSize (area ));err != nil { 562return fmt .Errorf ("add has size constraint: %w" ,err ) 563 } 564 } 565 566case Len : 567length := int (constraint ) 568 569if _ ,err := s .Add (lengthSizeEq ,segment .sizeEqConst (length ));err != nil { 570return fmt .Errorf ("add has int size constraint: %w" ,err ) 571 } 572 573case Percent : 574f := float64 (constraint )/ 100 575 576if _ ,err := s .Add (percentSizeEq ,segment .sizeEqScaledSize (area ,f ));err != nil { 577return fmt .Errorf ("add has size constraint: %w" ,err ) 578 } 579 580case Ratio : 581f := float64 (constraint .Num )/ float64 (max (1 ,constraint .Den )) 582 583if _ ,err := s .Add (ratioSizeEq ,segment .sizeEqScaledSize (area ,f ));err != nil { 584return fmt .Errorf ("add has size constraint: %w" ,err ) 585 } 586 587case Fill : 588if _ ,err := s .Add (fillGrow ,segment .sizeEqSize (area ));err != nil { 589return fmt .Errorf ("add has size constraint: %w" ,err ) 590 } 591 } 592 } 593 594return nil 595} 596 597func configureFlexConstraints ( 598s * casso.Solver , 599area element , 600spacers []element , 601flex Flex , 602spacing int , 603)error { 604var spacersExceptFirstAndLast []element 605 606if len (spacers )> 2 { 607spacersExceptFirstAndLast = spacers [1 :len (spacers )- 1 ] 608 } 609 610switch flex { 611case FlexLegacy : 612for _ ,sp := range spacersExceptFirstAndLast { 613if _ ,err := s .Add (spacerSizeEq ,sp .sizeEqConst (spacing ));err != nil { 614return fmt .Errorf ("add has size constraint: %w" ,err ) 615 } 616 } 617 618if len (spacers )>= 2 { 619first ,last := spacers [0 ],spacers [len (spacers )- 1 ] 620 621if _ ,err := s .Add (required - weak ,first .empty ());err != nil { 622return fmt .Errorf ("add constraints: %w" ,err ) 623 } 624 625if _ ,err := s .Add (required - weak ,last .empty ());err != nil { 626return fmt .Errorf ("add constraints: %w" ,err ) 627 } 628 } 629 630case FlexSpaceEvenly : 631for _ ,indices := range combinations (len (spacers ),2 ) { 632i ,j := indices [0 ],indices [1 ] 633 634left ,right := spacers [i ],spacers [j ] 635 636if _ ,err := s .Add (spacerSizeEq ,left .sizeEqSize (right ));err != nil { 637return fmt .Errorf ("add has size constraint: %w" ,err ) 638 } 639 } 640 641for _ ,sp := range spacers { 642if _ ,err := s .Add (spacerSizeEq ,sp .sizeGTE (spacing ));err != nil { 643return fmt .Errorf ("add constraints: %w" ,err ) 644 } 645 646if _ ,err := s .Add (spaceGrow ,sp .sizeEqSize (area ));err != nil { 647return fmt .Errorf ("add constraints: %w" ,err ) 648 } 649 } 650 651case FlexSpaceAround : 652if len (spacers )<= 2 { 653for _ ,indices := range combinations (len (spacers ),2 ) { 654i ,j := indices [0 ],indices [1 ] 655 656left ,right := spacers [i ],spacers [j ] 657 658if _ ,err := s .Add (spacerSizeEq ,left .sizeEqSize (right ));err != nil { 659return fmt .Errorf ("add has size constraint: %w" ,err ) 660 } 661 } 662 663for _ ,sp := range spacers { 664if _ ,err := s .Add (spacerSizeEq ,sp .sizeGTE (spacing ));err != nil { 665return fmt .Errorf ("add constraints: %w" ,err ) 666 } 667 668if _ ,err := s .Add (spaceGrow ,sp .sizeEqSize (area ));err != nil { 669return fmt .Errorf ("add constraints: %w" ,err ) 670 } 671 } 672 }else { 673first ,rest := spacers [0 ],spacers [1 :] 674last ,middle := rest [len (rest )- 1 ],rest [:len (rest )- 1 ] 675 676for _ ,indices := range combinations (len (middle ),2 ) { 677i ,j := indices [0 ],indices [1 ] 678 679left ,right := middle [i ],middle [j ] 680 681if _ ,err := s .Add (spacerSizeEq ,left .sizeEqSize (right ));err != nil { 682return fmt .Errorf ("add has size constraint: %w" ,err ) 683 } 684 } 685 686if len (middle )> 0 { 687firstMiddle := middle [0 ] 688 689for _ ,e := range []element {first ,last } { 690if _ ,err := s .Add (spacerSizeEq ,firstMiddle .sizeEqDouble (e ));err != nil { 691return fmt .Errorf ("add has double size constraint: %w" ,err ) 692 } 693 } 694 } 695 696for _ ,sp := range spacers { 697if _ ,err := s .Add (spacerSizeEq ,sp .sizeGTE (spacing ));err != nil { 698return fmt .Errorf ("add has min size constraint: %w" ,err ) 699 } 700 701if _ ,err := s .Add (spaceGrow ,sp .sizeEqSize (area ));err != nil { 702return fmt .Errorf ("add has size constraint: %w" ,err ) 703 } 704 } 705 } 706 707case FlexSpaceBetween : 708for _ ,indices := range combinations (len (spacersExceptFirstAndLast ),2 ) { 709i ,j := indices [0 ],indices [1 ] 710 711left ,right := spacersExceptFirstAndLast [i ],spacersExceptFirstAndLast [j ] 712 713if _ ,err := s .Add (spacerSizeEq ,left .sizeEqSize (right ));err != nil { 714return fmt .Errorf ("add has size constraint: %w" ,err ) 715 } 716 } 717 718for _ ,sp := range spacersExceptFirstAndLast { 719if _ ,err := s .Add (spacerSizeEq ,sp .sizeGTE (spacing ));err != nil { 720return fmt .Errorf ("add constraints: %w" ,err ) 721 } 722 723if _ ,err := s .Add (spaceGrow ,sp .sizeEqSize (area ));err != nil { 724return fmt .Errorf ("add constraints: %w" ,err ) 725 } 726 } 727 728if len (spacers )>= 2 { 729first ,last := spacers [0 ],spacers [len (spacers )- 1 ] 730 731if _ ,err := s .Add (required - weak ,first .empty ());err != nil { 732return fmt .Errorf ("add constraints: %w" ,err ) 733 } 734 735if _ ,err := s .Add (required - weak ,last .empty ());err != nil { 736return fmt .Errorf ("add constraints: %w" ,err ) 737 } 738 } 739 740case FlexStart : 741for _ ,sp := range spacersExceptFirstAndLast { 742if _ ,err := s .Add (spacerSizeEq ,sp .sizeEqConst (spacing ));err != nil { 743return fmt .Errorf ("add has size constraint: %w" ,err ) 744 } 745 } 746 747if len (spacers )>= 2 { 748first := spacers [0 ] 749last := spacers [len (spacers )- 1 ] 750 751if _ ,err := s .Add (required - weak ,first .empty ());err != nil { 752return fmt .Errorf ("add constraints: %w" ,err ) 753 } 754 755if _ ,err := s .Add (grow ,last .sizeEqSize (area ));err != nil { 756return fmt .Errorf ("add constraints: %w" ,err ) 757 } 758 } 759 760case FlexCenter : 761for _ ,sp := range spacersExceptFirstAndLast { 762if _ ,err := s .Add (spacerSizeEq ,sp .sizeEqConst (spacing ));err != nil { 763return fmt .Errorf ("add has size constraint: %w" ,err ) 764 } 765 } 766 767if len (spacers )>= 2 { 768first ,last := spacers [0 ],spacers [len (spacers )- 1 ] 769 770if _ ,err := s .Add (grow ,first .sizeEqSize (area ));err != nil { 771return fmt .Errorf ("add constraints: %w" ,err ) 772 } 773 774if _ ,err := s .Add (grow ,last .sizeEqSize (area ));err != nil { 775return fmt .Errorf ("add constraints: %w" ,err ) 776 } 777 778if _ ,err := s .Add (spacerSizeEq ,first .sizeEqSize (last ));err != nil { 779return fmt .Errorf ("add constraints: %w" ,err ) 780 } 781 } 782 783case FlexEnd : 784for _ ,sp := range spacersExceptFirstAndLast { 785if _ ,err := s .Add (spacerSizeEq ,sp .sizeEqConst (spacing ));err != nil { 786return fmt .Errorf ("add has size constraint: %w" ,err ) 787 } 788 } 789 790if len (spacers )>= 2 { 791first := spacers [0 ] 792last := spacers [len (spacers )- 1 ] 793 794if _ ,err := s .Add (required - weak ,last .empty ());err != nil { 795return fmt .Errorf ("add constraints: %w" ,err ) 796 } 797 798if _ ,err := s .Add (grow ,first .sizeEqSize (area ));err != nil { 799return fmt .Errorf ("add constraints: %w" ,err ) 800 } 801 } 802 } 803 804return nil 805} 806 807func configureVariableConstraints ( 808s * casso.Solver , 809variables []casso.Symbol , 810)error { 811// ┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐ 812// │ │ │ │ │ │ │ │ 813// v v v v v v v v 814// ┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐┌──────────────────┐┌ ┐ 815// │ Max(20) │ │ Max(20) │ │ Max(20) │ 816// └ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘└──────────────────┘└ ┘ 817// ^ ^ ^ ^ ^ ^ ^ ^ 818// └v0 └v1 └v2 └v3 └v4 └v5 └v6 └v7 819 820variables = variables [1 :] 821 822count := len (variables ) 823 824for i := 0 ;i < count - count % 2 ;i += 2 { 825left ,right := variables [i ],variables [i + 1 ] 826 827if _ ,err := s .Add (required ,casso .NewConstraint (casso .LTE ,0 ,left .T (1 ),right .T (- 1 )));err != nil { 828return fmt .Errorf ("add constraint: %w" ,err ) 829 } 830 } 831 832return nil 833} 834 835func configureVariableInAreaConstraints ( 836s * casso.Solver , 837variables []casso.Symbol , 838area element , 839)error { 840for _ ,v := range variables { 841if _ ,err := s .Add (required ,casso .NewConstraint (casso .GTE ,0 ,v .T (1 ),area .start .T (- 1 )));err != nil { 842return fmt .Errorf ("add start constraint: %w" ,err ) 843 } 844 845if _ ,err := s .Add (required ,casso .NewConstraint (casso .LTE ,0 ,v .T (1 ),area .end .T (- 1 )));err != nil { 846return fmt .Errorf ("add end constraint: %w" ,err ) 847 } 848 } 849 850return nil 851} 852 853func configureArea ( 854s * casso.Solver , 855area element , 856areaStart ,areaEnd float64 , 857)error { 858if _ ,err := s .Add (required ,casso .NewConstraint (casso .EQ ,- areaStart ,area .start .T (1 )));err != nil { 859return fmt .Errorf ("add start constraint: %w" ,err ) 860 } 861 862if _ ,err := s .Add (required ,casso .NewConstraint (casso .EQ ,- areaEnd ,area .end .T (1 )));err != nil { 863return fmt .Errorf ("add end constraint: %w" ,err ) 864 } 865 866return nil 867} 868 869func newElements (variables []casso.Symbol ) []element { 870count := len (variables ) 871 872elements := make ([]element ,0 ,count / 2 + 1 ) 873 874for i := 0 ;i < count - count % 2 ;i += 2 { 875s ,e := variables [i ],variables [i + 1 ] 876 877elements = append (elements ,element {start :s ,end :e }) 878 } 879 880return elements 881} 882 883type element struct { 884start ,end casso.Symbol 885} 886 887func (e element )empty () casso.Constraint { 888return casso .NewConstraint (casso .EQ ,0 ,e .end .T (1 ),e .start .T (- 1 )) 889} 890 891func (e element )sizeEqConst (size int ) casso.Constraint { 892return casso .NewConstraint (casso .EQ ,- float64 (size )* floatPrecisionMultiplier ,e .end .T (1 ),e .start .T (- 1 )) 893} 894 895func (e element )sizeLTE (size int ) casso.Constraint { 896return casso .NewConstraint (casso .LTE ,- float64 (size )* floatPrecisionMultiplier ,e .end .T (1 ),e .start .T (- 1 )) 897} 898 899func (e element )sizeGTE (size int ) casso.Constraint { 900return casso .NewConstraint (casso .GTE ,- float64 (size )* floatPrecisionMultiplier ,e .end .T (1 ),e .start .T (- 1 )) 901} 902 903func (e element )sizeEqSize (other element ) casso.Constraint { 904return casso .NewConstraint (casso .EQ ,0 ,e .end .T (1 ),e .start .T (- 1 ),other .end .T (- 1 ),other .start .T (1 )) 905} 906 907func (e element )sizeEqScaledSize (other element ,f float64 ) casso.Constraint { 908return casso .NewConstraint (casso .EQ ,0 ,e .end .T (1 ),e .start .T (- 1 ),other .end .T (- f ),other .start .T (f )) 909} 910 911func (e element )sizeEqDouble (other element ) casso.Constraint { 912return casso .NewConstraint (casso .EQ ,0 ,e .end .T (1 ),e .start .T (- 1 ),other .end .T (- 2 ),other .start .T (2 )) 913} 914 915func combinations (n ,k int ) [][]int { 916combs := binomial (n ,k ) 917data := make ([][]int ,combs ) 918if len (data )== 0 { 919return nil 920 } 921 922data [0 ]= make ([]int ,k ) 923for i := range data [0 ] { 924data [0 ][i ]= i 925 } 926 927for i := 1 ;i < combs ;i ++ { 928next := make ([]int ,k ) 929copy (next ,data [i - 1 ]) 930nextCombination (next ,n ,k ) 931data [i ]= next 932 } 933 934return data 935} 936 937func nextCombination (s []int ,n ,k int ) { 938for j := k - 1 ;j >= 0 ;j -- { 939if s [j ]== n + j - k { 940continue 941 } 942 943s [j ]++ 944 945for l := j + 1 ;l < k ;l ++ { 946s [l ]= s [j ]+ l - j 947 } 948 949break 950 } 951} 952 953func binomial (n ,k int )int { 954if n < 0 || k < 0 { 955panic ("layout: binomial: negative input" ) 956 } 957 958if n < k { 959return 0 960 } 961 962// (n,k) = (n, n-k) 963if k > n / 2 { 964k = n - k 965 } 966 967b := 1 968for i := 1 ;i <= k ;i ++ { 969b = (n - k + i )* b / i 970 } 971 972return b 973}