Skip to content

Instantly share code, notes, and snippets.

@lithdew
Last active May 31, 2020 13:20
Show Gist options
  • Save lithdew/250fde08aa4e08ce4211f74365268e17 to your computer and use it in GitHub Desktop.
Save lithdew/250fde08aa4e08ce4211f74365268e17 to your computer and use it in GitHub Desktop.
casso: Split a region up into vertically-aligned regions that meet a set of constraints.
package layout
import "github.com/lithdew/casso"
type Layout struct {
solver *casso.Solver
tags []casso.Symbol
err error
}
func New(solver *casso.Solver) Layout {
return Layout{solver: solver}
}
func (a *Layout) Required(constraints ...casso.Constraint) { a.Apply(casso.Required, constraints...) }
func (a *Layout) Strong(constraints ...casso.Constraint) { a.Apply(casso.Strong, constraints...) }
func (a *Layout) Medium(constraints ...casso.Constraint) { a.Apply(casso.Medium, constraints...) }
func (a *Layout) Weak(constraints ...casso.Constraint) { a.Apply(casso.Weak, constraints...) }
func (a *Layout) Apply(priority casso.Priority, constraints ...casso.Constraint) {
if a.err != nil {
return
}
for _, constraint := range constraints {
tag, err := a.solver.AddConstraintWithPriority(priority, constraint)
if err != nil {
a.err = err
return
}
a.tags = append(a.tags, tag)
}
}
func (a *Layout) Finalize() error {
err := a.err
if err != nil {
a.Destroy()
}
return err
}
func (a *Layout) Destroy() {
for _, tag := range a.tags {
_ = a.solver.RemoveConstraint(tag)
}
a.tags = a.tags[:0]
}
package layout
import (
"github.com/lithdew/casso"
"github.com/stretchr/testify/require"
"testing"
)
func TestSplitVertical(t *testing.T) {
cases := []struct {
input Rect
params []Constraint
expected []Rect
}{
{
input: Rect{x: 2, y: 2, w: 10, h: 10},
params: []Constraint{Length(5), Min(0)},
expected: []Rect{
{x: 2, y: 2, w: 10, h: 5},
{x: 2, y: 7, w: 10, h: 5},
},
},
{
input: Rect{x: 2, y: 2, w: 10, h: 10},
params: []Constraint{Ratio(1, 3), Ratio(2, 3)},
expected: []Rect{
{x: 2, y: 2, w: 10, h: 3},
{x: 2, y: 5, w: 10, h: 7},
},
},
{
input: Rect{x: 2, y: 2, w: 10, h: 10},
params: []Constraint{Percentage(40), Length(1), Min(0)},
expected: []Rect{
{x: 2, y: 2, w: 10, h: 4},
{x: 2, y: 6, w: 10, h: 1},
{x: 2, y: 7, w: 10, h: 5},
},
},
{
input: Rect{x: 2, y: 2, w: 10, h: 10},
params: []Constraint{Percentage(10), Max(5), Min(1)},
expected: []Rect{
{x: 2, y: 2, w: 10, h: 1},
{x: 2, y: 3, w: 10, h: 5},
{x: 2, y: 8, w: 10, h: 4},
},
},
}
for _, test := range cases {
actual, err := SplitVertical(test.input, test.params...)
require.NoError(t, err, test)
require.EqualValues(t, test.expected, actual, test)
}
}
func SplitVertical(r Rect, constraints ...Constraint) ([]Rect, error) {
results := make([]Rect, len(constraints))
elements := make([]Element, len(constraints))
solver := casso.NewSolver()
layout := New(solver)
for i := 0; i < len(constraints); i++ {
// w >= 0
// h >= 0
// x >= r.x
// y >= r.y
// x + w >= r.x + r.w
// y + h >= r.y + r.h
// first.y + first.h == second.y
// first.y == r.y
// last.y + last.h == r.y + r.h
// x == r.x
// w == r.w
// apply constraint
elements[i].x = casso.New()
elements[i].y = casso.New()
elements[i].w = casso.New()
elements[i].h = casso.New()
layout.Required(GTE(0, elements[i].w.T(1)))
layout.Required(GTE(0, elements[i].h.T(1)))
layout.Required(GTE(-float64(r.x), elements[i].x.T(1)))
layout.Required(GTE(-float64(r.y), elements[i].y.T(1)))
layout.Required(LTE(-float64(r.x+r.w), elements[i].x.T(1), elements[i].w.T(1)))
layout.Required(LTE(-float64(r.y+r.h), elements[i].y.T(1), elements[i].h.T(1)))
if i > 0 {
layout.Required(EQ(0, elements[i-1].y.T(1), elements[i-1].h.T(1), elements[i].y.T(-1)))
}
if i == 0 {
layout.Required(EQ(-float64(r.y), elements[0].y.T(1)))
}
if i == len(constraints)-1 {
layout.Required(EQ(-float64(r.y+r.h), elements[len(elements)-1].y.T(1), elements[len(elements)-1].h.T(1)))
}
layout.Required(EQ(-float64(r.x), elements[i].x.T(1)))
layout.Required(EQ(-float64(r.w), elements[i].w.T(1)))
layout.Weak(constraints[i](r.h, elements[i].h))
}
if err := layout.Finalize(); err != nil {
return nil, err
}
for i := 0; i < len(elements); i++ {
results[i].x = int(solver.Val(elements[i].x))
results[i].y = int(solver.Val(elements[i].y))
results[i].w = int(solver.Val(elements[i].w))
results[i].h = int(solver.Val(elements[i].h))
}
return results, nil
}
type Rect struct {
x int
y int
w int
h int
}
func (r Rect) PadLeft(pad int) Rect {
if r.w < pad {
return r
}
r.x += pad
r.w -= pad
return r
}
func (r Rect) PadRight(pad int) Rect {
if r.w < pad {
return r
}
r.w -= pad
return r
}
func (r Rect) PadHorizontal(pad int) Rect {
if r.w < 2*pad {
return r
}
r.x += pad
r.w -= 2 * pad
return r
}
func (r Rect) PadTop(pad int) Rect {
if r.h < pad {
return r
}
r.y += pad
r.h -= pad
return r
}
func (r Rect) PadBottom(pad int) Rect {
if r.h < pad {
return r
}
r.h -= pad
return r
}
func (r Rect) PadVertical(pad int) Rect {
if r.h < 2*pad {
return r
}
r.y += pad
r.h -= 2 * pad
return r
}
func (r Rect) Pad(pad int) Rect {
if r.w < 2*pad || r.h < 2*pad {
return r
}
r.x += pad
r.y += pad
r.w -= 2 * pad
r.h -= 2 * pad
return r
}
type Element struct {
x casso.Symbol
y casso.Symbol
w casso.Symbol
h casso.Symbol
}
type Constraint func(a int, b casso.Symbol) casso.Constraint
func Percentage(val int) Constraint {
if val < 0 || val > 100 {
panic("BUG: Percentage(val int) may only be called with number in range [0, 100].")
}
return func(a int, b casso.Symbol) casso.Constraint {
return EQ(-float64(val*a/100), b.T(1))
}
}
func Ratio(num, den int) Constraint {
return func(a int, b casso.Symbol) casso.Constraint {
return EQ(-float64(a*num/den), b.T(1))
}
}
func Min(min int) Constraint {
return func(_ int, b casso.Symbol) casso.Constraint {
return GTE(-float64(min), b.T(1))
}
}
func Max(max int) Constraint {
return func(_ int, b casso.Symbol) casso.Constraint {
return LTE(-float64(max), b.T(1))
}
}
func Length(val int) Constraint {
return func(_ int, b casso.Symbol) casso.Constraint {
return EQ(-float64(val), b.T(1))
}
}
func EQ(constant float64, terms ...casso.Term) casso.Constraint {
return casso.NewConstraint(casso.EQ, constant, terms...)
}
func GTE(constant float64, terms ...casso.Term) casso.Constraint {
return casso.NewConstraint(casso.GTE, constant, terms...)
}
func LTE(constant float64, terms ...casso.Term) casso.Constraint {
return casso.NewConstraint(casso.LTE, constant, terms...)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment