From bc948d1a61f17fa61200a50b6edba2c27690ad97 Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Wed, 14 Feb 2024 16:00:49 -0600 Subject: [PATCH] Restyle and limit scopes for quicker navigation --- README.md | 2 +- kiwi.lua | 873 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 449 insertions(+), 426 deletions(-) diff --git a/README.md b/README.md index b6a9217..acfabe9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ print(right_edge:value()) -- 500 ``` -In addition to the expression builder there are convenience constructors: `new_pair_ratio_constraint`, `new_pair_constraint`, and `new_single_constraint` to allow efficient construction of the most common simple expression types for GUI layout. +In addition to the expression builder there is a convenience constraints submodule with: `pair_ratio`, `pair`, and `single` to allow efficient construction of the most common simple expression types for GUI layout. ## Documentation WIP - However the API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server. diff --git a/kiwi.lua b/kiwi.lua index d8c2204..4f07c0e 100644 --- a/kiwi.lua +++ b/kiwi.lua @@ -198,7 +198,7 @@ end ---@param coeff number? ---@nodiscard local function new_expr_one(constant, var, coeff) - return ffi_gc(new_expr_one_temp(constant, var, coeff), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] ---@diagnostic disable-line: param-type-mismatch + return ffi_gc(new_expr_one_temp(constant, var, coeff), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] end ---@param constant number @@ -227,6 +227,7 @@ local REQUIRED = Strength.REQUIRED ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param op kiwi.RelOp ---@param strength? number +---@nodiscard local function rel(lhs, rhs, op, strength) local function to_expr(o) if ffi_istype(Expression, o) then @@ -264,7 +265,7 @@ end ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ----@return kiwi.Constraint +---@nodiscard function kiwi.le(lhs, rhs, strength) return rel(lhs, rhs, "LE", strength) end @@ -273,7 +274,7 @@ end ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ----@return kiwi.Constraint +---@nodiscard function kiwi.ge(lhs, rhs, strength) return rel(lhs, rhs, "GE", strength) end @@ -282,84 +283,89 @@ end ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ----@return kiwi.Constraint +---@nodiscard function kiwi.eq(lhs, rhs, strength) return rel(lhs, rhs, "EQ", strength) end ---- Variables are the values the constraint solver calculates. ----@class kiwi.Var: ffi.ctype* ----@overload fun(name: string): kiwi.Var ----@operator mul(number): kiwi.Term ----@operator div(number): kiwi.Term ----@operator unm: kiwi.Term ----@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression ----@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression -local Var_cls = { - le = kiwi.le, - ge = kiwi.ge, - eq = kiwi.eq, +do + --- Variables are the values the constraint solver calculates. + ---@class kiwi.Var: ffi.cdata* + ---@overload fun(name: string): kiwi.Var + ---@operator mul(number): kiwi.Term + ---@operator div(number): kiwi.Term + ---@operator unm: kiwi.Term + ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + local Var_cls = { + le = kiwi.le, + ge = kiwi.ge, + eq = kiwi.eq, - --- Change the name of the variable. - ---@type fun(self: kiwi.Var, name: string) - set_name = ckiwi.kiwi_var_set_name, + --- Change the name of the variable. + ---@type fun(self: kiwi.Var, name: string) + set_name = ckiwi.kiwi_var_set_name, - --- Get the current value of the variable. - ---@type fun(self: kiwi.Var): number - value = ckiwi.kiwi_var_value, + --- Get the current value of the variable. + ---@type fun(self: kiwi.Var): number + value = ckiwi.kiwi_var_value, - --- Set the value of the variable. - ---@type fun(self: kiwi.Var, value: number) - set = ckiwi.kiwi_var_set_value, -} + --- Set the value of the variable. + ---@type fun(self: kiwi.Var, value: number) + set = ckiwi.kiwi_var_set_value, + } ---- Get the name of the variable. ----@return string -function Var_cls:name() - return ffi_string(ckiwi.kiwi_var_name(self)) -end + --- Get the name of the variable. + ---@return string + ---@nodiscard + function Var_cls:name() + return ffi_string(ckiwi.kiwi_var_name(self)) + end ---- Create a term from this variable. ----@param coefficient number? ----@return kiwi.Term -function Var_cls:toterm(coefficient) - return Term(self, coefficient or 1.0) -end + --- Create a term from this variable. + ---@param coefficient number? + ---@return kiwi.Term + ---@nodiscard + function Var_cls:toterm(coefficient) + return Term(self, coefficient) + end ---- Create a term from this variable. ----@param coefficient number? ----@param constant number? ----@return kiwi.Expression -function Var_cls:toexpr(coefficient, constant) - return new_expr_one(constant or 0.0, self, coefficient) -end + --- Create a term from this variable. + ---@param coefficient number? + ---@param constant number? + ---@return kiwi.Expression + ---@nodiscard + function Var_cls:toexpr(coefficient, constant) + return new_expr_one(constant or 0.0, self, coefficient) + end -ffi.metatype(Var, { - __index = Var_cls, + local Var_mt = { + __index = Var_cls, + } - __new = function(_, name) + function Var_mt:__new(name) return ffi_gc(ckiwi.kiwi_var_new(name), ckiwi.kiwi_var_del) - end, + end - __mul = function(a, b) + function Var_mt.__mul(a, b) if type(a) == "number" then return Term(b, a) elseif type(b) == "number" then return Term(a, b) end error("Invalid var *") - end, + end - __div = function(a, b) + function Var_mt.__div(a, b) assert(type(b) == "number", "Invalid var /") return Term(a, 1.0 / b) - end, + end - __unm = function(var) - return Term(var, -1.0) - end, + function Var_mt:__unm() + return Term(self, -1.0) + end - __add = function(a, b) + function Var_mt.__add(a, b) if ffi_istype(Var, b) then if type(a) == "number" then return new_expr_one(a, b) @@ -374,74 +380,77 @@ ffi.metatype(Var, { return new_expr_one(b, a) end error("Invalid var +") - end, + end - __sub = function(a, b) - return a + -b - end, + function Var_mt.__sub(a, b) + return Var_mt.__add(a, -b) + end - __tostring = function(var) - return var:name() .. "(" .. var:value() .. ")" - end, -}) + function Var_mt:__tostring() + return self:name() .. "(" .. self:value() .. ")" + end ---- Terms are the components of an expression. ---- Each term is a variable multiplied by a constant coefficient (default 1.0). ----@class kiwi.Term: ffi.ctype* ----@overload fun(var: kiwi.Var, coefficient: number?): kiwi.Term ----@field var kiwi.Var ----@field coefficient number ----@operator mul(number): kiwi.Term ----@operator div(number): kiwi.Term ----@operator unm: kiwi.Term ----@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression ----@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression -local Term_cls = { - le = kiwi.le, - ge = kiwi.ge, - eq = kiwi.eq, -} - ----@return number -function Term_cls:value() - return self.coefficient * self.var:value() + ffi.metatype(Var, Var_mt) end ---- Create an expression from this term. ----@param constant number? ----@return kiwi.Expression -function Term_cls:toexpr(constant) - return new_expr_one(constant or 0.0, self.var, self.coefficient) -end +do + --- Terms are the components of an expression. + --- Each term is a variable multiplied by a constant coefficient (default 1.0). + ---@class kiwi.Term: ffi.cdata* + ---@overload fun(var: kiwi.Var, coefficient: number?): kiwi.Term + ---@field var kiwi.Var + ---@field coefficient number + ---@operator mul(number): kiwi.Term + ---@operator div(number): kiwi.Term + ---@operator unm: kiwi.Term + ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + local Term_cls = { + le = kiwi.le, + ge = kiwi.ge, + eq = kiwi.eq, + } -ffi.metatype(Term, { - __index = Term_cls, + ---@return number + ---@nodiscard + function Term_cls:value() + return self.coefficient * self.var:value() + end - __new = function(_, var, coefficient) - return ffi_gc(ffi_new(Term, ckiwi.kiwi_var_clone(var), coefficient or 1.0), function(term) + --- Create an expression from this term. + ---@param constant number? + ---@return kiwi.Expression + function Term_cls:toexpr(constant) + return new_expr_one(constant or 0.0, self.var, self.coefficient) + end + + local Term_mt = { __index = Term_cls } + + function Term_mt.__new(T, var, coefficient) + return ffi_gc(ffi_new(T, ckiwi.kiwi_var_clone(var), coefficient or 1.0), function(term) ckiwi.kiwi_var_del(term.var) end) - end, + end - __mul = function(a, b) + function Term_mt.__mul(a, b) if type(b) == "number" then return Term(a.var, a.coefficient * b) elseif type(a) == "number" then return Term(b.var, b.coefficient * a) end error("Invalid term *") - end, + end - __div = function(term, denom) - assert(type(denom) == "number", "Invalid term /") - return Term(term.var, term.coefficient / denom) - end, + function Term_mt.__div(a, b) + assert(type(b) == "number", "Invalid term /") + return Term(a.var, a.coefficient / b) + end - __unm = function(term) - return Term(term.var, -term.coefficient) - end, + function Term_mt:__unm() + return Term(self.var, -self.coefficient) + end - __add = function(a, b) + function Term_mt.__add(a, b) if ffi_istype(Var, b) then return new_expr_pair(0.0, a.var, b, a.coefficient) elseif ffi_istype(Term, b) then @@ -456,18 +465,37 @@ ffi.metatype(Term, { return new_expr_one(b, a.var, a.coefficient) end error("Invalid term + op") - end, + end - __sub = function(a, b) - return a + -b - end, + function Term_mt.__sub(a, b) + return Term_mt.__add(a, -b) + end - __tostring = function(term) - return tostring(term.coefficient) .. " " .. term.var:name() - end, -}) + function Term_mt:__tostring() + return tostring(self.coefficient) .. " " .. self.var:name() + end + + ffi.metatype(Term, Term_mt) +end do + --- Expressions are a sum of terms with an added constant. + ---@class kiwi.Expression: ffi.cdata* + ---@overload fun(terms: kiwi.Term[], constant: number?): kiwi.Expression + ---@field constant number + ---@field package term_count number + ---@field package terms_ ffi.cdata* + ---@operator mul(number): kiwi.Expression + ---@operator div(number): kiwi.Expression + ---@operator unm: kiwi.Expression + ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression + local Expression_cls = { + le = kiwi.le, + ge = kiwi.ge, + eq = kiwi.eq, + } + ---@param expr kiwi.Expression ---@param constant number ---@nodiscard @@ -526,23 +554,6 @@ do return ret end - --- Expressions are a sum of terms with an added constant. - ---@class kiwi.Expression: ffi.ctype* - ---@overload fun(terms: kiwi.Term[], constant: number?): kiwi.Expression - ---@field constant number - ---@field package term_count number - ---@field package terms_ ffi.cdata* - ---@operator mul(number): kiwi.Expression - ---@operator div(number): kiwi.Expression - ---@operator unm: kiwi.Expression - ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression - ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression - local Expression_cls = { - le = kiwi.le, - ge = kiwi.ge, - eq = kiwi.eq, - } - ---@return number ---@nodiscard function Expression_cls:value() @@ -570,178 +581,120 @@ do return new_expr_constant(self, self.constant) end - ffi.metatype(Expression, { + local Expression_mt = { __index = Expression_cls, + } - __new = function(_, terms, constant) - local e = ffi_gc(ffi_new(Expression, #terms), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] - for i, t in ipairs(terms) do - local dt = e.terms_[i - 1] --[[@as kiwi.Term]] - dt.var = ckiwi.kiwi_var_clone(t.var) - dt.coefficient = t.coefficient - end - e.constant = constant or 0.0 - e.term_count = #terms - return e - end, - - __mul = function(a, b) - if type(a) == "number" then - return mul_expr_constant(b, a) - elseif type(b) == "number" then - return mul_expr_constant(a, b) - end - error("Invalid expr *") - end, - - __div = function(expr, denom) - assert(type(denom) == "number", "Invalid expr /") - return mul_expr_constant(expr, 1.0 / denom) - end, - - __unm = function(expr) - return mul_expr_constant(expr, -1.0) - end, - - __add = function(a, b) - if ffi_istype(Var, b) then - return add_expr_term(a, b) - elseif ffi_istype(Expression, b) then - if type(a) == "number" then - return new_expr_constant(b, a + b.constant) - else - return add_expr_expr(a, b) - end - elseif ffi_istype(Term, b) then - return add_expr_term(a, b.var, b.coefficient) - elseif type(b) == "number" then - return new_expr_constant(a, a.constant + b) - end - error("Invalid expr +") - end, - - __sub = function(a, b) - return a + -b - end, - - __tostring = function(expr) - local tab = new_tab(expr.term_count + 1, 0) - for i = 0, expr.term_count - 1 do - tab[i + 1] = tostring(expr.terms_[i]) - end - tab[expr.term_count + 1] = expr.constant - return concat(tab, " + ") - end, - }) -end - ---- A constraint is a linear inequality or equality with associated strength. ---- Constraints can be built with arbitrary left and right hand expressions. But ---- ultimately they all have the form `expression [op] 0`. ----@class kiwi.Constraint: ffi.ctype* ----@overload fun(lhs: kiwi.Expression, rhs: kiwi.Expression?, op: kiwi.RelOp?, strength: number?): kiwi.Constraint -local Constraint_cls = { - --- The strength of the constraint. - ---@type fun(self: kiwi.Constraint): number - strength = ckiwi.kiwi_constraint_strength, - - --- The relational operator of the constraint. - ---@type fun(self: kiwi.Constraint): kiwi.RelOp - op = ckiwi.kiwi_constraint_op, - - --- Whether the constraint is violated in the current solution. - ---@type fun(self: kiwi.Constraint): boolean - violated = ckiwi.kiwi_constraint_violated, -} - ---- The reduced expression defining the constraint. ----@return kiwi.Expression ----@nodiscard -function Constraint_cls:expression() - local SZ = 8 - local expr = ffi_new(Expression, SZ) --[[@as kiwi.Expression]] - local n = ckiwi.kiwi_constraint_expression(self, expr, SZ) - if n > SZ then - expr = ffi_new(Expression, n) --[[@as kiwi.Expression]] - n = ckiwi.kiwi_constraint_expression(self, expr, n) + function Expression_mt.__new(T, terms, constant) + local e = ffi_gc(ffi_new(T, #terms), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] + for i, t in ipairs(terms) do + local dt = e.terms_[i - 1] --[[@as kiwi.Term]] + dt.var = ckiwi.kiwi_var_clone(t.var) + dt.coefficient = t.coefficient + end + e.constant = constant or 0.0 + e.term_count = #terms + return e end - return ffi_gc(expr, ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] ---@diagnostic disable-line: param-type-mismatch + + function Expression_mt.__mul(a, b) + if type(a) == "number" then + return mul_expr_constant(b, a) + elseif type(b) == "number" then + return mul_expr_constant(a, b) + end + error("Invalid expr *") + end + + function Expression_mt.__div(a, b) + assert(type(b) == "number", "Invalid expr /") + return mul_expr_constant(a, 1.0 / b) + end + + function Expression_mt:__unm() + return mul_expr_constant(self, -1.0) + end + + function Expression_mt.__add(a, b) + if ffi_istype(Var, b) then + return add_expr_term(a, b) + elseif ffi_istype(Expression, b) then + if type(a) == "number" then + return new_expr_constant(b, a + b.constant) + else + return add_expr_expr(a, b) + end + elseif ffi_istype(Term, b) then + return add_expr_term(a, b.var, b.coefficient) + elseif type(b) == "number" then + return new_expr_constant(a, a.constant + b) + end + error("Invalid expr +") + end + + function Expression_mt.__sub(a, b) + return Expression_mt.__add(a, -b) + end + + function Expression_mt:__tostring() + local tab = new_tab(self.term_count + 1, 0) + for i = 0, self.term_count - 1 do + tab[i + 1] = tostring(self.terms_[i]) + end + tab[self.term_count + 1] = self.constant + return concat(tab, " + ") + end + + ffi.metatype(Expression, Expression_mt) end ---- Create a constraint between a pair of variables with ratio. ---- The constraint is of the form `left [op|==] coeff right + [constant|0.0]`. ----@param left kiwi.Var ----@param coeff number right side term coefficient ----@param right kiwi.Var ----@param constant number? constant (default 0.0) ----@param op kiwi.RelOp? relational operator (default "EQ") ----@param strength number? strength (default REQUIRED) ----@return kiwi.Constraint ----@nodiscard -function kiwi.new_pair_ratio_constraint(left, coeff, right, constant, op, strength) - assert(ffi_istype(Var, left) and ffi_istype(Var, right)) - local lhs = ffi_new(Expression, 2) --[[@as kiwi.Expression]] - local dt = lhs.terms_[0] - dt.var = left - dt.coefficient = 1.0 - dt = lhs.terms_[1] - dt.var = right - dt.coefficient = -coeff - lhs.constant = -(constant or 0.0) - lhs.term_count = 2 - return Constraint(lhs, nil, op, strength) -end +do + --- A constraint is a linear inequality or equality with associated strength. + --- Constraints can be built with arbitrary left and right hand expressions. But + --- ultimately they all have the form `expression [op] 0`. + ---@class kiwi.Constraint: ffi.cdata* + ---@overload fun(lhs: kiwi.Expression, rhs: kiwi.Expression?, op: kiwi.RelOp?, strength: number?): kiwi.Constraint + local Constraint_cls = { + --- The strength of the constraint. + ---@type fun(self: kiwi.Constraint): number + strength = ckiwi.kiwi_constraint_strength, -local new_pair_ratio_constraint = kiwi.new_pair_ratio_constraint + --- The relational operator of the constraint. + ---@type fun(self: kiwi.Constraint): kiwi.RelOp + op = ckiwi.kiwi_constraint_op, ---- Create a constraint between a pair of variables with ratio. ---- The constraint is of the form `left [op|==] right + [constant|0.0]`. ----@param left kiwi.Var ----@param right kiwi.Var ----@param constant number? constant (default 0.0) ----@param op kiwi.RelOp? relational operator (default "EQ") ----@param strength number? strength (default REQUIRED) ----@return kiwi.Constraint ----@nodiscard -function kiwi.new_pair_constraint(left, right, constant, op, strength) - return new_pair_ratio_constraint(left, 1.0, right, constant, op, strength) -end + --- Whether the constraint is violated in the current solution. + ---@type fun(self: kiwi.Constraint): boolean + violated = ckiwi.kiwi_constraint_violated, + } ---- Create a single term constraint ---- The constraint is of the form `var [op|==] [constant|0.0]`. ----@param var kiwi.Var ----@param constant number? constant (default 0.0) ----@param op kiwi.RelOp? relational operator (default "EQ") ----@param strength number? strength (default REQUIRED) ----@return kiwi.Constraint ----@nodiscard -function kiwi.new_single_constraint(var, constant, op, strength) - assert(ffi_istype(Var, var)) - local lhs = ffi_new(Expression, 1) --[[@as kiwi.Expression]] - local dt = lhs.terms_[0] - dt.var = var - dt.coefficient = 1.0 - lhs.constant = -(constant or 0.0) - lhs.term_count = 1 - return Constraint(lhs, nil, op, strength) -end + --- The reduced expression defining the constraint. + ---@return kiwi.Expression + ---@nodiscard + function Constraint_cls:expression() + local SZ = 8 + local expr = ffi_new(Expression, SZ) --[[@as kiwi.Expression]] + local n = ckiwi.kiwi_constraint_expression(self, expr, SZ) + if n > SZ then + expr = ffi_new(Expression, n) --[[@as kiwi.Expression]] + n = ckiwi.kiwi_constraint_expression(self, expr, n) + end + return ffi_gc(expr, ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] + end -ffi.metatype(Constraint, { - __index = Constraint_cls, + local Constraint_mt = { + __index = Constraint_cls, + } - __new = function(_, lhs, rhs, op, strength) + function Constraint_mt:__new(lhs, rhs, op, strength) return ffi_gc( ckiwi.kiwi_constraint_new(lhs, rhs, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) - end, + end - __tostring = function(self) - local ops = { - [0] = "<=", - ">=", - "==", - } + function Constraint_mt:__tostring() + local ops = { [0] = "<=", ">=", "==" } local strengths = { [Strength.REQUIRED] = "required", [Strength.STRONG] = "strong", @@ -749,158 +702,228 @@ ffi.metatype(Constraint, { [Strength.WEAK] = "weak", } local strength = self:strength() - return strformat( - "%s %s 0 | %s", - tostring(self:expression()), - ops[tonumber(self:op())], - strengths[strength] or tostring(strength) - ) - end, -}) + local strength_str = strengths[strength] or tostring(strength) + local op = ops[tonumber(self:op())] + return strformat("%s %s 0 | %s", tostring(self:expression()), op, strength_str) + end -local Error_mt = { - __tostring = function(self) - return strformat("%s: (%s, %s)", self.message, self.solver, self.item) - end, -} - ----@param kind kiwi.ErrKind ----@param message string ----@param solver kiwi.Solver ----@param item any -local function new_error(kind, message, solver, item) - ---@class kiwi.Error - ---@field kind kiwi.ErrKind - ---@field message string - ---@field solver kiwi.Solver? - ---@field item any? - return setmetatable({ - kind = kind, - message = message, - solver = solver, - item = item, - }, Error_mt) + ffi.metatype(Constraint, Constraint_mt) end -local C = ffi.C +do + local constraints = {} + kiwi.constraints = constraints ----@class kiwi.KiwiErr: ffi.ctype* ----@field package kind kiwi.ErrKind ----@field package message ffi.cdata* ----@field package must_free boolean ----@overload fun(): kiwi.KiwiErr -local KiwiErr = ffi.typeof("struct KiwiErr") --[[@as kiwi.KiwiErr]] + --- Create a constraint between a pair of variables with ratio. + --- The constraint is of the form `left [op|==] coeff right + [constant|0.0]`. + ---@param left kiwi.Var + ---@param coeff number right side term coefficient + ---@param right kiwi.Var + ---@param constant number? constant (default 0.0) + ---@param op kiwi.RelOp? relational operator (default "EQ") + ---@param strength number? strength (default REQUIRED) + ---@return kiwi.Constraint + ---@nodiscard + function constraints.pair_ratio(left, coeff, right, constant, op, strength) + assert(ffi_istype(Var, left) and ffi_istype(Var, right)) + local lhs = ffi_new(Expression, 2) --[[@as kiwi.Expression]] + local dt = lhs.terms_[0] + dt.var = left + dt.coefficient = 1.0 + dt = lhs.terms_[1] + dt.var = right + dt.coefficient = -coeff + lhs.constant = -(constant or 0.0) + lhs.term_count = 2 ----@param f fun(solver: kiwi.Solver, item: any, ...): kiwi.KiwiErr? ----@param solver kiwi.Solver ----@param item any -local function try_solver(f, solver, item, ...) - local err = f(solver, item, ...) - if err ~= nil then - local kind = err.kind - local message = err.message ~= nil and ffi_string(err.message) or "" - if err.must_free then - C.free(err) - end - error(new_error(kind, message, solver, item)) + return ffi_gc( + ckiwi.kiwi_constraint_new(lhs, nil, op or "EQ", strength or REQUIRED), + ckiwi.kiwi_constraint_del + ) --[[@as kiwi.Constraint]] + end + + local pair_ratio = constraints.pair_ratio + + --- Create a constraint between a pair of variables with ratio. + --- The constraint is of the form `left [op|==] right + [constant|0.0]`. + ---@param left kiwi.Var + ---@param right kiwi.Var + ---@param constant number? constant (default 0.0) + ---@param op kiwi.RelOp? relational operator (default "EQ") + ---@param strength number? strength (default REQUIRED) + ---@return kiwi.Constraint + ---@nodiscard + function constraints.pair(left, right, constant, op, strength) + return pair_ratio(left, 1.0, right, constant, op, strength) + end + + --- Create a single term constraint + --- The constraint is of the form `var [op|==] [constant|0.0]`. + ---@param var kiwi.Var + ---@param constant number? constant (default 0.0) + ---@param op kiwi.RelOp? relational operator (default "EQ") + ---@param strength number? strength (default REQUIRED) + ---@return kiwi.Constraint + ---@nodiscard + function constraints.single(var, constant, op, strength) + assert(ffi_istype(Var, var)) + return ffi_gc( + ckiwi.kiwi_constraint_new( + new_expr_one_temp(-(constant or 0.0), var, 1.0), + nil, + op or "EQ", + strength or REQUIRED + ), + ckiwi.kiwi_constraint_del + ) --[[@as kiwi.Constraint]] end end ----@class kiwi.Solver: ffi.ctype* ----@overload fun(): kiwi.Solver -local Solver_cls = { - --- Test whether a constraint is in the solver. - ---@type fun(self: kiwi.Solver, constraint: kiwi.Constraint): boolean - has_constraint = ckiwi.kiwi_solver_has_constraint, +do + local C = ffi.C - --- Test whether an edit variable has been added to the solver. - ---@type fun(self: kiwi.Solver, var: kiwi.Var): boolean - has_edit_var = ckiwi.kiwi_solver_has_edit_var, + ---@class kiwi.KiwiErr: ffi.cdata* + ---@field package kind kiwi.ErrKind + ---@field package message ffi.cdata* + ---@field package must_free boolean + ---@overload fun(): kiwi.KiwiErr + local KiwiErr = ffi.typeof("struct KiwiErr") --[[@as kiwi.KiwiErr]] - --- Update the values of the external solver variables. - ---@type fun(self: kiwi.Solver) - update_vars = ckiwi.kiwi_solver_update_vars, + local Error_mt = { + __tostring = function(self) + return strformat("%s: (%s, %s)", self.message, self.solver, self.item) + end, + } - --- Reset the solver to the empty starting conditions. + ---@param kind kiwi.ErrKind + ---@param message string + ---@param solver kiwi.Solver + ---@param item any + local function new_error(kind, message, solver, item) + ---@class kiwi.Error + ---@field kind kiwi.ErrKind + ---@field message string + ---@field solver kiwi.Solver? + ---@field item any? + return setmetatable({ + kind = kind, + message = message, + solver = solver, + item = item, + }, Error_mt) + end + + ---@param f fun(solver: kiwi.Solver, item: any, ...): kiwi.KiwiErr? + ---@param solver kiwi.Solver + ---@param item any + local function try_solver(f, solver, item, ...) + local err = f(solver, item, ...) + if err ~= nil then + local kind = err.kind + local message = err.message ~= nil and ffi_string(err.message) or "" + if err.must_free then + C.free(err) + end + error(new_error(kind, message, solver, item)) + end + end + + ---@class kiwi.Solver: ffi.cdata* + ---@overload fun(): kiwi.Solver + local Solver_cls = { + --- Test whether a constraint is in the solver. + ---@type fun(self: kiwi.Solver, constraint: kiwi.Constraint): boolean + has_constraint = ckiwi.kiwi_solver_has_constraint, + + --- Test whether an edit variable has been added to the solver. + ---@type fun(self: kiwi.Solver, var: kiwi.Var): boolean + has_edit_var = ckiwi.kiwi_solver_has_edit_var, + + --- Update the values of the external solver variables. + ---@type fun(self: kiwi.Solver) + update_vars = ckiwi.kiwi_solver_update_vars, + + --- Reset the solver to the empty starting conditions. + --- + --- This method resets the internal solver state to the empty starting + --- condition, as if no constraints or edit variables have been added. + --- This can be faster than deleting the solver and creating a new one + --- when the entire system must change, since it can avoid unecessary + --- heap (de)allocations. + ---@type fun(self: kiwi.Solver) + reset = ckiwi.kiwi_solver_reset, + + --- Dump a representation of the solver to stdout. + ---@type fun(self: kiwi.Solver) + dump = ckiwi.kiwi_solver_dump, + } + + --- Adds a constraint to the solver. + --- Raises + --- KiwiErrDuplicateConstraint: The given constraint has already been added to the solver. + --- KiwiErrUnsatisfiableConstraint: The given constraint is required and cannot be satisfied. + ---@param constraint kiwi.Constraint + function Solver_cls:add_constraint(constraint) + try_solver(ckiwi.kiwi_solver_add_constraint, self, constraint) + end + + --- Removes a constraint from the solver. + --- Raises + --- KiwiErrUnknownConstraint: The given constraint has not been added to the solver. + ---@param constraint kiwi.Constraint + function Solver_cls:remove_constraint(constraint) + try_solver(ckiwi.kiwi_solver_remove_constraint, self, constraint) + end + + --- Adds an edit variable to the solver. --- - --- This method resets the internal solver state to the empty starting - --- condition, as if no constraints or edit variables have been added. - --- This can be faster than deleting the solver and creating a new one - --- when the entire system must change, since it can avoid unecessary - --- heap (de)allocations. - ---@type fun(self: kiwi.Solver) - reset = ckiwi.kiwi_solver_reset, + --- This method should be called before the `suggestValue` method is + --- used to supply a suggested value for the given edit variable. + --- Raises + --- KiwiErrDuplicateEditVariable: The given edit variable has already been added to the solver. + --- KiwiErrBadRequiredStrength: The given strength is >= required. + ---@param var kiwi.Var the variable to add as an edit variable + ---@param strength number the strength of the edit variable (must be less than `Strength.REQUIRED`) + function Solver_cls:add_edit_var(var, strength) + try_solver(ckiwi.kiwi_solver_add_edit_var, self, var, strength) + end - --- Dump a representation of the solver to stdout. - ---@type fun(self: kiwi.Solver) - dump = ckiwi.kiwi_solver_dump, -} + --- Remove an edit variable from the solver. + --- Raises + --- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver + ---@param var kiwi.Var the edit variable to remove + function Solver_cls:remove_edit_var(var) + try_solver(ckiwi.kiwi_solver_remove_edit_var, self, var) + end ---- Adds a constraint to the solver. ---- Raises ---- KiwiErrDuplicateConstraint: The given constraint has already been added to the solver. ---- KiwiErrUnsatisfiableConstraint: The given constraint is required and cannot be satisfied. ----@param constraint kiwi.Constraint -function Solver_cls:add_constraint(constraint) - try_solver(ckiwi.kiwi_solver_add_constraint, self, constraint) + --- Suggest a value for the given edit variable. + --- This method should be used after an edit variable has been added to the solver in order + --- to suggest the value for that variable. After all suggestions have been made, + --- the `update_vars` methods can be used to update the values of the external solver variables. + --- Raises + --- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver. + ---@param var kiwi.Var the edit variable to suggest a value for + ---@param value number the suggested value + function Solver_cls:suggest_value(var, value) + try_solver(ckiwi.kiwi_solver_suggest_value, self, var, value) + end + + --- Dump a representation of the solver to a string. + ---@return string + ---@nodiscard + function Solver_cls:dumps() + local cs = ckiwi.kiwi_solver_dumps(self) + local s = ffi_string(cs) + C.free(cs) + return s + end + + kiwi.Solver = ffi.metatype("struct KiwiSolver", { + __index = Solver_cls, + __new = function(_) + return ffi_gc(ckiwi.kiwi_solver_new(), ckiwi.kiwi_solver_del) + end, + }) --[[@as kiwi.Solver]] end ---- Removes a constraint from the solver. ---- Raises ---- KiwiErrUnknownConstraint: The given constraint has not been added to the solver. ----@param constraint kiwi.Constraint -function Solver_cls:remove_constraint(constraint) - try_solver(ckiwi.kiwi_solver_remove_constraint, self, constraint) -end - ---- Adds an edit variable to the solver. ---- ---- This method should be called before the `suggestValue` method is ---- used to supply a suggested value for the given edit variable. ---- Raises ---- KiwiErrDuplicateEditVariable: The given edit variable has already been added to the solver. ---- KiwiErrBadRequiredStrength: The given strength is >= required. ----@param var kiwi.Var the variable to add as an edit variable ----@param strength number the strength of the edit variable (must be less than `Strength.REQUIRED`) -function Solver_cls:add_edit_var(var, strength) - try_solver(ckiwi.kiwi_solver_add_edit_var, self, var, strength) -end - ---- Remove an edit variable from the solver. ---- Raises ---- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver ----@param var kiwi.Var the edit variable to remove -function Solver_cls:remove_edit_var(var) - try_solver(ckiwi.kiwi_solver_remove_edit_var, self, var) -end - ---- Suggest a value for the given edit variable. ---- This method should be used after an edit variable has been added to the solver in order ---- to suggest the value for that variable. After all suggestions have been made, ---- the `update_vars` methods can be used to update the values of the external solver variables. ---- Raises ---- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver. ----@param var kiwi.Var the edit variable to suggest a value for ----@param value number the suggested value -function Solver_cls:suggest_value(var, value) - try_solver(ckiwi.kiwi_solver_suggest_value, self, var, value) -end - ---- Dump a representation of the solver to a string. ----@return string ----@nodiscard -function Solver_cls:dumps() - local cs = ckiwi.kiwi_solver_dumps(self) - local s = ffi_string(cs) - C.free(cs) - return s -end - -kiwi.Solver = ffi.metatype("struct KiwiSolver", { - __index = Solver_cls, - __new = function(_) - return ffi_gc(ckiwi.kiwi_solver_new(), ckiwi.kiwi_solver_del) - end, -}) --[[@as kiwi.Solver]] - return kiwi