Files
ljkiwi/kiwi.lua

930 lines
30 KiB
Lua

local kiwi = {}
local ffi = require("ffi")
local ckiwi
do
local cpath, err = package.searchpath("ckiwi", package.cpath)
if cpath == nil then
error("kiwi dynamic library 'ckiwi' not found\n" .. err)
end
ckiwi = ffi.load(cpath)
end
ffi.cdef([[
enum KiwiErrKind {
KiwiErrNone,
KiwiErrUnsatisfiableConstraint = 1,
KiwiErrUnknownConstraint,
KiwiErrDuplicateConstraint,
KiwiErrUnknownEditVariable,
KiwiErrDuplicateEditVariable,
KiwiErrBadRequiredStrength,
KiwiErrInternalSolverError,
KiwiErrAlloc,
KiwiErrNullObject,
KiwiErrUnknown,
};
enum KiwiRelOp { LE, GE, EQ };
typedef struct KiwiVarRefType* KiwiVarRef;
typedef struct KiwiConstraintRefType* KiwiConstraintRef;
typedef struct KiwiTerm {
KiwiVarRef var;
double coefficient;
} KiwiTerm;
typedef struct KiwiExpression {
double constant;
int term_count;
KiwiTerm terms_[?];
} KiwiExpression;
typedef struct KiwiErr {
enum KiwiErrKind kind;
const char* message;
bool must_free;
} KiwiErr;
typedef struct KiwiSolver KiwiSolver;
KiwiVarRef kiwi_var_new(const char* name);
void kiwi_var_del(KiwiVarRef var);
KiwiVarRef kiwi_var_clone(KiwiVarRef var);
const char* kiwi_var_name(KiwiVarRef var);
void kiwi_var_set_name(KiwiVarRef var, const char* name);
double kiwi_var_value(KiwiVarRef var);
void kiwi_var_set_value(KiwiVarRef var, double value);
int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other);
void kiwi_expression_del_vars(KiwiExpression* expr);
KiwiConstraintRef kiwi_constraint_new(
const KiwiExpression* lhs,
const KiwiExpression* rhs,
enum KiwiRelOp op,
double strength
);
void kiwi_constraint_del(KiwiConstraintRef constraint);
KiwiConstraintRef kiwi_constraint_clone(KiwiConstraintRef constraint);
double kiwi_constraint_strength(KiwiConstraintRef constraint);
enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint);
bool kiwi_constraint_violated(KiwiConstraintRef constraint);
int kiwi_constraint_expression(KiwiConstraintRef constraint, KiwiExpression* out, int out_size);
KiwiSolver* kiwi_solver_new();
void kiwi_solver_del(KiwiSolver* sp);
const KiwiErr* kiwi_solver_add_constraint(KiwiSolver* sp, KiwiConstraintRef constraint);
const KiwiErr* kiwi_solver_remove_constraint(KiwiSolver* sp, KiwiConstraintRef constraint);
bool kiwi_solver_has_constraint(const KiwiSolver* sp, KiwiConstraintRef constraint);
const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* sp, KiwiVarRef var, double strength);
const KiwiErr* kiwi_solver_remove_edit_var(KiwiSolver* sp, KiwiVarRef var);
bool kiwi_solver_has_edit_var(const KiwiSolver* sp, KiwiVarRef var);
const KiwiErr* kiwi_solver_suggest_value(KiwiSolver* sp, KiwiVarRef var, double value);
void kiwi_solver_update_vars(KiwiSolver* sp);
void kiwi_solver_reset(KiwiSolver* sp);
void kiwi_solver_dump(const KiwiSolver* sp);
char* kiwi_solver_dumps(const KiwiSolver* sp);
void free(void *);
]])
local strformat = string.format
local ffi_gc, ffi_istype, ffi_new, ffi_string = ffi.gc, ffi.istype, ffi.new, ffi.string
local concat = table.concat
local has_table_new, new_tab = pcall(require, "table.new")
if not has_table_new or type(new_tab) ~= "function" then
new_tab = function()
return {}
end
end
---@alias kiwi.ErrKind
---| '"KiwiErrNone"' # No error.
---| '"KiwiErrUnsatisfiableConstraint"' # The given constraint is required and cannot be satisfied.
---| '"KiwiErrUnknownConstraint"' # The given constraint has not been added to the solver.
---| '"KiwiErrDuplicateConstraint"' # The given constraint has already been added to the solver.
---| '"KiwiErrUnknownEditVariable"' # The given edit variable has not been added to the solver.
---| '"KiwiErrDuplicateEditVariable"' # The given edit variable has already been added to the solver.
---| '"KiwiErrBadRequiredStrength"' # The given strength is >= required.
---| '"KiwiErrInternalSolverError"' # An internal solver error occurred.
---| '"KiwiErrAlloc"' # A memory allocation error occurred.
---| '"KiwiErrNullObject"' # A method was invoked on a null or empty object.
---| '"KiwiErrUnknown"' # An unknown error occurred.
kiwi.ErrKind = ffi.typeof("enum KiwiErrKind") --[[@as kiwi.ErrKind]]
---@alias kiwi.RelOp
---| '"LE"' # <= (less than or equal)
---| '"GE"' # >= (greater than or equal)
---| '"EQ"' # == (equal)
kiwi.RelOp = ffi.typeof("enum KiwiRelOp")
kiwi.Strength = {
REQUIRED = 1001001000.0,
STRONG = 1000000.0,
MEDIUM = 1000.0,
WEAK = 1.0,
}
--- Create a custom constraint strength.
---@param a number: Scale factor 1e6
---@param b number: Scale factor 1e3
---@param c number: Scale factor 1
---@param w? number: Weight
---@return number
---@nodiscard
function kiwi.Strength.create(a, b, c, w)
local function clamp(n)
return math.max(0, math.min(1000, n))
end
w = w or 1.0
return clamp(a * w) * 1000000.0 + clamp(b * w) * 1000.0 + clamp(c * w)
end
local Var = ffi.typeof("struct KiwiVarRefType") --[[@as kiwi.Var]]
kiwi.Var = Var
local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]]
kiwi.Term = Term
local Expression = ffi.typeof("struct KiwiExpression") --[[@as kiwi.Expression]]
kiwi.Expression = Expression
local Constraint = ffi.typeof("struct KiwiConstraintRefType") --[[@as kiwi.Constraint]]
kiwi.Constraint = Constraint
---@param expr kiwi.Expression
---@param var kiwi.Var
---@param coeff number?
---@nodiscard
local function add_expr_term(expr, var, coeff)
local ret = ffi_gc(ffi_new(Expression, expr.term_count + 1), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
for i = 0, expr.term_count - 1 do
local st = expr.terms_[i] --[[@as kiwi.Term]]
local dt = ret.terms_[i] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(st.var)
dt.coefficient = st.coefficient
end
local dt = ret.terms_[expr.term_count]
dt.var = ckiwi.kiwi_var_clone(var)
dt.coefficient = coeff or 1.0
ret.constant = expr.constant
ret.term_count = expr.term_count + 1
return ret
end
---@param constant number
---@param var kiwi.Var
---@param coeff number?
---@nodiscard
local function new_expr_one_temp(constant, var, coeff)
local ret = ffi_new(Expression, 1) --[[@as kiwi.Expression]]
local dt = ret.terms_[0]
dt.var = ckiwi.kiwi_var_clone(var)
dt.coefficient = coeff or 1.0
ret.constant = constant
ret.term_count = 1
return ret
end
---@param constant number
---@param var kiwi.Var
---@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]]
end
---@param constant number
---@param var1 kiwi.Var
---@param var2 kiwi.Var
---@param coeff1 number?
---@param coeff2 number?
---@nodiscard
local function new_expr_pair(constant, var1, var2, coeff1, coeff2)
local ret = ffi_gc(ffi_new(Expression, 2), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
local dt = ret.terms_[0]
dt.var = ckiwi.kiwi_var_clone(var1)
dt.coefficient = coeff1 or 1.0
dt = ret.terms_[1]
dt.var = ckiwi.kiwi_var_clone(var2)
dt.coefficient = coeff2 or 1.0
ret.constant = constant
ret.term_count = 2
return ret
end
local Strength = kiwi.Strength
local REQUIRED = Strength.REQUIRED
---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@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
return o --[[@as kiwi.Expression]]
elseif type(o) == "number" then
if o == 0 then
return nil
end
local ret = ffi_new(Expression, 0) --[[@as kiwi.Expression]]
ret.constant = o
ret.term_count = 0
return ret
end
local var
local coeff = 1.0
if ffi_istype(Var, o) then
var = o --[[@as kiwi.Var]]
elseif ffi_istype(Term, o) then
var = o.var
coeff = o.coefficient
else
error("Expected Expression|Term|Var|number, got " .. type(o) .. " instead")
end
return new_expr_one_temp(0.0, var, coeff)
end
return ffi_gc(
ckiwi.kiwi_constraint_new(to_expr(lhs), to_expr(rhs), op, strength or REQUIRED),
ckiwi.kiwi_constraint_del
) --[[@as kiwi.Constraint]]
end
--- Define a constraint with expressions as `a <= b`.
---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param strength? number
---@nodiscard
function kiwi.le(lhs, rhs, strength)
return rel(lhs, rhs, "LE", strength)
end
--- Define a constraint with expressions as `a >= b`.
---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param strength? number
---@nodiscard
function kiwi.ge(lhs, rhs, strength)
return rel(lhs, rhs, "GE", strength)
end
--- Define a constraint with expressions as `a == b`.
---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number
---@param strength? number
---@nodiscard
function kiwi.eq(lhs, rhs, strength)
return rel(lhs, rhs, "EQ", strength)
end
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,
--- 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,
}
--- 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
---@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
---@nodiscard
function Var_cls:toexpr(coefficient, constant)
return new_expr_one(constant or 0.0, self, coefficient)
end
local Var_mt = {
__index = Var_cls,
}
function Var_mt:__new(name)
return ffi_gc(ckiwi.kiwi_var_new(name), ckiwi.kiwi_var_del)
end
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
function Var_mt.__div(a, b)
assert(type(b) == "number", "Invalid var /")
return Term(a, 1.0 / b)
end
function Var_mt:__unm()
return Term(self, -1.0)
end
function Var_mt.__add(a, b)
if ffi_istype(Var, b) then
if type(a) == "number" then
return new_expr_one(a, b)
else
return new_expr_pair(0.0, a, b)
end
elseif ffi_istype(Term, b) then
return new_expr_pair(0.0, b.var, a, b.coefficient)
elseif ffi_istype(Expression, b) then
return add_expr_term(b, a)
elseif type(b) == "number" then
return new_expr_one(b, a)
end
error("Invalid var +")
end
function Var_mt.__sub(a, b)
return Var_mt.__add(a, -b)
end
function Var_mt:__tostring()
return self:name() .. "(" .. self:value() .. ")"
end
ffi.metatype(Var, Var_mt)
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,
}
---@return number
---@nodiscard
function Term_cls:value()
return self.coefficient * self.var:value()
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
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
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
function Term_mt.__div(a, b)
assert(type(b) == "number", "Invalid term /")
return Term(a.var, a.coefficient / b)
end
function Term_mt:__unm()
return Term(self.var, -self.coefficient)
end
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
if type(a) == "number" then
return new_expr_one(a, b.var, b.coefficient)
else
return new_expr_pair(0.0, a.var, b.var, a.coefficient, b.coefficient)
end
elseif ffi_istype(Expression, b) then
return add_expr_term(b, a.var, a.coefficient)
elseif type(b) == "number" then
return new_expr_one(b, a.var, a.coefficient)
end
error("Invalid term + op")
end
function Term_mt.__sub(a, b)
return Term_mt.__add(a, -b)
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
local function mul_expr_constant(expr, constant)
local ret = ffi_gc(ffi_new(Expression, expr.term_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
for i = 0, expr.term_count - 1 do
local st = expr.terms_[i] --[[@as kiwi.Term]]
local dt = ret.terms_[i] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(st.var)
dt.coefficient = st.coefficient * constant
end
ret.constant = expr.constant * constant
ret.term_count = expr.term_count
return ret
end
---@param a kiwi.Expression
---@param b kiwi.Expression
---@nodiscard
local function add_expr_expr(a, b)
local a_count = a.term_count
local b_count = b.term_count
local ret = ffi_gc(ffi_new(Expression, a_count + b_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
for i = 0, a_count - 1 do
local dt = ret.terms_[i] --[[@as kiwi.Term]]
local st = a.terms_[i] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(st.var)
dt.coefficient = st.coefficient
end
for i = 0, b_count - 1 do
local dt = ret.terms_[a_count + i] --[[@as kiwi.Term]]
local st = b.terms_[i] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(st.var)
dt.coefficient = st.coefficient
end
ret.constant = a.constant + b.constant
ret.term_count = a_count + b_count
return ret
end
---@param expr kiwi.Expression
---@param constant number
---@nodiscard
local function new_expr_constant(expr, constant)
local ret = ffi_gc(ffi_new(Expression, expr.term_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
for i = 0, expr.term_count - 1 do
local dt = ret.terms_[i] --[[@as kiwi.Term]]
local st = expr.terms_[i] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(st.var)
dt.coefficient = st.coefficient
end
ret.constant = constant
ret.term_count = expr.term_count
return ret
end
---@return number
---@nodiscard
function Expression_cls:value()
local sum = self.constant
for i = 0, self.term_count - 1 do
sum = sum + self.terms_[i]:value()
end
return sum
end
---@return kiwi.Term[]
---@nodiscard
function Expression_cls:terms()
local terms = new_tab(self.term_count, 0)
for i = 0, self.term_count - 1 do
local t = self.terms_[i] --[[@as kiwi.Term]]
terms[i + 1] = Term(t.var, t.coefficient)
end
return terms
end
---@return kiwi.Expression
---@nodiscard
function Expression_cls:copy()
return new_expr_constant(self, self.constant)
end
local Expression_mt = {
__index = Expression_cls,
}
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
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
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,
--- 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)
end
return ffi_gc(expr, ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
end
local Constraint_mt = {
__index = Constraint_cls,
}
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
function Constraint_mt:__tostring()
local ops = { [0] = "<=", ">=", "==" }
local strengths = {
[Strength.REQUIRED] = "required",
[Strength.STRONG] = "strong",
[Strength.MEDIUM] = "medium",
[Strength.WEAK] = "weak",
}
local strength = self:strength()
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
ffi.metatype(Constraint, Constraint_mt)
end
do
local constraints = {}
kiwi.constraints = constraints
--- 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
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
do
local C = ffi.C
---@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]]
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)
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 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]]
end
return kiwi