Make sure that all cdata references for variables is tracked. This is done by attaching finalizers to Expression and Term objects. Some effort is made to avoid creating tracked temporary objects (esp Terms) now.
852 lines
27 KiB
Lua
852 lines
27 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 };
|
|
|
|
struct KiwiVarRefType;
|
|
struct KiwiConstraintRefType;
|
|
|
|
typedef struct KiwiVarRefType* KiwiVarRef;
|
|
typedef struct KiwiConstraintRefType* KiwiConstraintRef;
|
|
|
|
struct KiwiTerm {
|
|
KiwiVarRef var;
|
|
double coefficient;
|
|
};
|
|
|
|
typedef struct KiwiExpression {
|
|
double constant;
|
|
int term_count;
|
|
struct KiwiTerm terms_[?];
|
|
}* KiwiExpressionPtr;
|
|
|
|
typedef const struct KiwiExpression* KiwiExpressionConstPtr;
|
|
|
|
typedef struct KiwiErr {
|
|
enum KiwiErrKind kind;
|
|
const char* message;
|
|
bool must_free;
|
|
} const* KiwiErrPtr;
|
|
|
|
struct KiwiSolver;
|
|
typedef struct KiwiSolver* KiwiSolverPtr;
|
|
|
|
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);
|
|
|
|
KiwiConstraintRef
|
|
kiwi_constraint_new(KiwiExpressionConstPtr expression, enum KiwiRelOp op, double strength);
|
|
void kiwi_constraint_del(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, KiwiExpressionPtr out, int out_size);
|
|
|
|
KiwiErrPtr kiwi_solver_add_constraint(KiwiSolverPtr sp, KiwiConstraintRef constraint);
|
|
KiwiErrPtr kiwi_solver_remove_constraint(KiwiSolverPtr sp, KiwiConstraintRef constraint);
|
|
bool kiwi_solver_has_constraint(KiwiSolverPtr sp, KiwiConstraintRef constraint);
|
|
KiwiErrPtr kiwi_solver_add_edit_var(KiwiSolverPtr sp, KiwiVarRef var, double strength);
|
|
KiwiErrPtr kiwi_solver_remove_edit_var(KiwiSolverPtr sp, KiwiVarRef var);
|
|
bool kiwi_solver_has_edit_var(KiwiSolverPtr sp, KiwiVarRef var);
|
|
KiwiErrPtr kiwi_solver_suggest_value(KiwiSolverPtr sp, KiwiVarRef var, double value);
|
|
void kiwi_solver_update_vars(KiwiSolverPtr sp);
|
|
void kiwi_solver_reset(KiwiSolverPtr sp);
|
|
void kiwi_solver_dump(KiwiSolverPtr sp);
|
|
char* kiwi_solver_dumps(KiwiSolverPtr sp);
|
|
|
|
KiwiSolverPtr kiwi_solver_new();
|
|
void kiwi_solver_del(KiwiSolverPtr sp);
|
|
|
|
void free(void *);
|
|
]])
|
|
|
|
local strformat = string.format
|
|
local ffi_cast, ffi_gc, ffi_istype, ffi_new, ffi_string =
|
|
ffi.cast, 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
|
|
|
|
--- Define a constraint with expressions as `a <= b`.
|
|
---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param strength? number
|
|
---@return kiwi.Constraint
|
|
function kiwi.le(a, b, strength)
|
|
return Constraint(a - b, "LE", strength)
|
|
end
|
|
|
|
--- Define a constraint with expressions as `a >= b`.
|
|
---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param strength? number
|
|
---@return kiwi.Constraint
|
|
function kiwi.ge(a, b, strength)
|
|
return Constraint(a - b, "GE", strength)
|
|
end
|
|
|
|
--- Define a constraint with expressions as `a == b`.
|
|
---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number
|
|
---@param strength? number
|
|
---@return kiwi.Constraint
|
|
function kiwi.eq(a, b, strength)
|
|
return Constraint(a - b, "EQ", strength)
|
|
end
|
|
|
|
local function expr_gc(expr)
|
|
local terms_ = expr.terms_
|
|
for i = 0, expr.term_count - 1 do
|
|
ckiwi.kiwi_var_del(terms_[i].var)
|
|
end
|
|
end
|
|
|
|
---@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), expr_gc) --[[@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(constant, var, coeff)
|
|
local ret = ffi_gc(ffi_new(Expression, 1), expr_gc) --[[@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 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), expr_gc) --[[@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
|
|
|
|
--- 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,
|
|
|
|
--- 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
|
|
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
|
|
|
|
ffi.metatype(Var, {
|
|
__index = Var_cls,
|
|
|
|
__new = function(_, name)
|
|
return ffi_gc(ckiwi.kiwi_var_new(name), ckiwi.kiwi_var_del)
|
|
end,
|
|
__gc = ckiwi.kiwi_var_del,
|
|
|
|
__mul = function(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,
|
|
|
|
__div = function(a, b)
|
|
assert(type(b) == "number", "Invalid var /")
|
|
return Term(a, 1.0 / b)
|
|
end,
|
|
|
|
__unm = function(var)
|
|
return Term(var, -1.0)
|
|
end,
|
|
|
|
__add = function(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,
|
|
|
|
__sub = function(a, b)
|
|
return a + -b
|
|
end,
|
|
|
|
__tostring = function(var)
|
|
return var:name() .. "(" .. var: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()
|
|
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
|
|
|
|
ffi.metatype(Term, {
|
|
__index = Term_cls,
|
|
|
|
__new = function(_, var, coefficient)
|
|
return ffi_gc(ffi_new(Term, ckiwi.kiwi_var_clone(var), coefficient or 1.0), function(term)
|
|
ckiwi.kiwi_var_del(term.var)
|
|
end)
|
|
end,
|
|
|
|
__mul = function(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,
|
|
|
|
__div = function(term, denom)
|
|
assert(type(denom) == "number", "Invalid term /")
|
|
return Term(term.var, term.coefficient / denom)
|
|
end,
|
|
|
|
__unm = function(term)
|
|
return Term(term.var, -term.coefficient)
|
|
end,
|
|
|
|
__add = function(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,
|
|
|
|
__sub = function(a, b)
|
|
return a + -b
|
|
end,
|
|
|
|
__tostring = function(term)
|
|
return tostring(term.coefficient) .. " " .. term.var:name()
|
|
end,
|
|
})
|
|
|
|
do
|
|
---@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), expr_gc) --[[@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), expr_gc) --[[@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), expr_gc) --[[@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
|
|
|
|
--- 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()
|
|
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
|
|
|
|
ffi.metatype(Expression, {
|
|
__index = Expression_cls,
|
|
|
|
__new = function(_, terms, constant)
|
|
local e = ffi_gc(ffi_new(Expression, #terms), expr_gc) --[[@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(expr: 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, expr_gc) --[[@as kiwi.Expression]] ---@diagnostic disable-line: param-type-mismatch
|
|
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))
|
|
return Constraint(new_expr_pair(-(constant or 0.0), right, left, -coeff), op, strength)
|
|
end
|
|
|
|
--- 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)
|
|
assert(ffi_istype(Var, left) and ffi_istype(Var, right))
|
|
return Constraint(new_expr_pair(-(constant or 0.0), right, left, -1.0), 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 kiwi.new_single_constraint(var, constant, op, strength)
|
|
assert(ffi_istype(Var, var))
|
|
return Constraint(new_expr_one(-(constant or 0.0), var), op, strength)
|
|
end
|
|
|
|
local Strength = kiwi.Strength
|
|
local REQUIRED = Strength.REQUIRED
|
|
|
|
ffi.metatype(Constraint, {
|
|
__index = Constraint_cls,
|
|
|
|
__new = function(_, expr, op, strength)
|
|
return ffi_gc(
|
|
ckiwi.kiwi_constraint_new(expr, op or "EQ", strength or REQUIRED),
|
|
ckiwi.kiwi_constraint_del
|
|
)
|
|
end,
|
|
|
|
__tostring = function(self)
|
|
local ops = {
|
|
[0] = "<=",
|
|
">=",
|
|
"==",
|
|
}
|
|
local strengths = {
|
|
[Strength.REQUIRED] = "required",
|
|
[Strength.STRONG] = "strong",
|
|
[Strength.MEDIUM] = "medium",
|
|
[Strength.WEAK] = "weak",
|
|
}
|
|
local strength = self:strength()
|
|
return strformat(
|
|
"%s %s 0 | %s",
|
|
self:expression(),
|
|
ops[tonumber(self:op())],
|
|
strengths[strength] or tostring(strength)
|
|
)
|
|
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)
|
|
end
|
|
|
|
local C = ffi.C
|
|
|
|
---@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]]
|
|
--
|
|
---@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.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,
|
|
|
|
--- 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,
|
|
|
|
__tostring = function(self)
|
|
return strformat("kiwi.Solver(0x%X)", ffi_cast("intptr_t", ffi_cast("void*", self)))
|
|
end,
|
|
}) --[[@as kiwi.Solver]]
|
|
|
|
return kiwi
|