Initial actual commit, awaiting CI

This commit is contained in:
2024-02-12 04:21:48 -06:00
parent 70a9213c91
commit 59bdeedc18
8 changed files with 1329 additions and 1 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
*.so
*.o
.cache/
compile_commands.json

View File

@@ -8,6 +8,5 @@
"lua_modules/share/lua/5.1/?.lua",
"lua_modules/share/lua/5.1/?/init.lua"
],
"workspace.library": ["lua_modules/share/lua/5.1"],
"workspace.checkThirdParty": false
}

32
Makefile Normal file
View File

@@ -0,0 +1,32 @@
SRCDIR ?= .
CC ?= $(CROSS)gcc
CFLAGS ?= -fPIC -O2
CFLAGS += -Wall -I$(SRCDIR)/kiwi
LIBFLAG ?= -shared
LIB_EXT ?= so
ifeq ($(findstring gcc, $(CC)), gcc)
CXX := $(subst gcc, g++, $(CC))
CXXFLAGS += -std=c++11
else
ifeq ($(CC), clang)
CXX := clang++
CXXFLAGS += -std=c++11
else
CXX := $(CC)
endif
endif
all: ckiwi.$(LIB_EXT)
install:
cp -f ckiwi.$(LIB_EXT) $(INST_LIBDIR)/ckiwi.$(LIB_EXT)
cp -f kiwi.lua $(INST_LUADIR)/kiwi.lua
clean:
rm -f ckiwi.$(LIB_EXT)
ckiwi.$(LIB_EXT): $(SRCDIR)/ckiwi/ckiwi.cpp
$(CXX) $(CXXFLAGS) $(CFLAGS) -fPIC -Wall -I$(SRCDIR)/kiwi $(LIBFLAG) -o $@ $<
.PHONY: all install clean

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
ljkiwi - Free LuaJIT FFI kiwi (Cassowary derived) constraint solver.
[![CI](https://github.com/jkl1337/ljkiwi/actions/workflows/ci.yml/badge.svg)](https://github.com/jkl1337/ljkiwi/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/jkl1337/ljkiwi/badge.svg?branch=master)](https://coveralls.io/github/jkl1337/ljkiwi?branch=master)
[![luarocks](https://img.shields.io/luarocks/v/jkl/ljkiwi)](https://luarocks.org/modules/jkl/ljkiwi)
# Introduction
Kiwi is a reasonably efficient C++ implementation of the Cassowary constraint solving algorithm. It is an implementation of the algorithm as described in the paper ["The Cassowary Linear Arithmetic Constraint Solving Algorithm"](http://www.cs.washington.edu/research/constraints/cassowary/techreports/cassowaryTR.pdf) by Greg J. Badros and Alan Borning. The Kiwi implementation is not based on the original C++ implementation, but is a ground-up reimplementation with performance 10x to 500x faster in typical use.
Cassowary constraint solving is a technique that is particularly well suited to user interface layout. It is the algorithm Apple uses for iOS and OS X Auto Layout.
There are a few Lua implementations or attempts. The SILE typesetting system has a pure Lua implementation of the original Cassowary code, which appears to be correct but is quite slow. There are two extant Lua ports of Kiwi, one that is based on a C rewrite of Kiwi. However testing of these was not encouraging with either segfaults or incorrect results.
Since the C++ Kiwi library is well tested and widely used it was simpler to provide a LuaJIT FFI wrapper and use that.
This package has no dependencies other than a C++11 toolchain to compile the included Kiwi library and a small C wrapper.
The Lua API has a pure Lua expression builder. There is of course some overhead to this, however in most cases expression building is infrequent and the underlying structures can be reused.
The wrapper is quite close to the Kiwi C++/Python port with a few naming changes.
## Example
```lua
local kiwi = require("kiwi")
local Var = kiwi.Var
local Button = setmetatable({}, {
__call = function(_, identifier)
return setmetatable({
left = Var(identifier .. " left"),
width = Var(identifier .. " width"),
}, {
__tostring = function(self)
return "Button(" .. self.left:value() .. ", " .. self.width:value() .. ")"
end,
})
end,
})
local b1 = Button("b1")
local b2 = Button("b2")
local left_edge = Var("left")
local right_edge = Var("width")
local STRONG = kiwi.Strength.STRONG
-- stylua: ignore start
local constraints = {
left_edge :eq(0.0),
-- two buttons are the same width
b1.width :eq(b2.width),
-- button1 starts 50 from the left margin
b1.left :eq(left_edge + 50),
-- button2 ends 50 from the right margin
right_edge :eq(b2.left + b2.width + 50),
-- button2 starts at least 100 from the end of button1. This is the "elastic" constraint
b2.left :ge(b1.left + b1.width + 100),
-- button1 has a minimum width of 87
b1.width :ge(87),
-- button1 has a preferred width of 87
b1.width :eq(87, STRONG),
-- button2 has minimum width of 113
b2.width :ge(113),
-- button2 has a preferred width of 113
b2.width :eq(113, STRONG),
}
-- stylua: ignore end
local solver = kiwi.Solver()
for _, c in ipairs(constraints) do
solver:add_constraint(c)
end
solver:update_vars()
print(b1) -- Button(50, 113)
print(b2) -- Button(263, 113)
print(left_edge:value()) -- 0
print(right_edge:value()) -- 426
solver:add_edit_var(right_edge, STRONG)
solver:suggest_value(right_edge, 500)
solver:update_vars()
print(b1) -- Button(50, 113)
print(b2) -- Button(337, 113)
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.
## Documentation
WIP - However the API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server.

258
ckiwi/ckiwi.cpp Normal file
View File

@@ -0,0 +1,258 @@
#include "ckiwi.h"
#include <kiwi/kiwi.h>
#include <cstring>
using namespace kiwi;
namespace {
template<class T, class CS>
class alignas(T) SharedRef {
private:
CS ref_;
public:
T* operator&() {
return reinterpret_cast<T*>(&ref_);
}
T* operator->() {
return reinterpret_cast<T*>(&ref_);
}
operator T&() {
return *reinterpret_cast<T*>(&ref_);
}
operator CS() const {
return ref_;
}
T& instance() {
return *reinterpret_cast<T*>(&ref_);
}
void destroy() {
instance().~T();
ref_ = {0};
}
SharedRef<T, CS>(CS ref) : ref_(ref) {}
template<typename... Args>
SharedRef<T, CS>(Args&&... args) {
new (&ref_) T(std::forward<Args>(args)...);
}
static_assert(sizeof(CS) >= sizeof(T), "SharedRef cannot wrap T (size)");
};
using ConstraintRef = SharedRef<Constraint, KiwiConstraintRef>;
using VariableRef = SharedRef<Variable, KiwiVarRef>;
KiwiErr make_error(KiwiErrKind kind, const std::exception& ex) {
constexpr auto max_n = sizeof(KiwiErr::message) - 1;
const auto n = std::min(std::strlen(ex.what()), max_n);
KiwiErr err {kind};
std::memcpy(err.message, ex.what(), n);
if (n == max_n)
err.message[max_n] = 0;
return err;
}
KiwiErr make_error(KiwiErrKind kind) {
return KiwiErr {kind, {0}};
}
template<typename F>
inline KiwiErr wrap_err(F&& f) {
try {
f();
} catch (const UnsatisfiableConstraint& err) {
return make_error(KiwiErrUnsatisfiableConstraint, err);
} catch (const UnknownConstraint& err) {
return make_error(KiwiErrUnknownConstraint, err);
} catch (const DuplicateConstraint& err) {
return make_error(KiwiErrDuplicateConstraint, err);
} catch (const UnknownEditVariable& err) {
return make_error(KiwiErrUnknownEditVariable, err);
} catch (const DuplicateEditVariable& err) {
return make_error(KiwiErrDuplicateEditVariable, err);
} catch (const BadRequiredStrength& err) {
return make_error(KiwiErrBadRequiredStrength, err);
} catch (const InternalSolverError& err) {
return make_error(KiwiErrInternalSolverError, err);
} catch (std::bad_alloc&) {
return make_error(KiwiErrAlloc);
} catch (const std::exception& err) {
return make_error(KiwiErrUnknown, err);
} catch (...) {
return make_error(KiwiErrUnknown);
}
return make_error(KiwiErrNone);
}
} // namespace
extern "C" {
KiwiVarRef kiwi_var_new(const char* name) {
return VariableRef(name);
}
void kiwi_var_del(KiwiVarRef var) {
VariableRef(var).destroy();
}
const char* kiwi_var_name(KiwiVarRef var) {
return VariableRef(var)->name().c_str();
}
void kiwi_var_set_name(KiwiVarRef var, const char* name) {
VariableRef(var)->setName(name);
}
double kiwi_var_value(KiwiVarRef var) {
return VariableRef(var)->value();
}
void kiwi_var_set_value(KiwiVarRef var, double value) {
VariableRef(var)->setValue(value);
}
int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other) {
return VariableRef(var)->equals(VariableRef(other));
}
KiwiConstraintRef kiwi_constraint_new(
const KiwiExpression* expression,
enum KiwiRelOp op,
double strength
) {
if (strength < 0.0) {
strength = kiwi::strength::required;
}
std::vector<Term> terms;
terms.reserve(expression->term_count);
for (auto* t = expression->terms; t != expression->terms + expression->term_count;
++t) {
terms.emplace_back(VariableRef(t->var), t->coefficient);
}
return ConstraintRef(
Expression(std::move(terms), expression->constant),
static_cast<RelationalOperator>(op),
strength
);
}
void kiwi_constraint_del(KiwiConstraintRef constraint) {
ConstraintRef(constraint).destroy();
}
double kiwi_constraint_strength(KiwiConstraintRef constraint) {
return ConstraintRef(constraint)->strength();
}
enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint) {
return static_cast<KiwiRelOp>(ConstraintRef(constraint)->op());
}
int kiwi_constraint_violated(KiwiConstraintRef constraint) {
return ConstraintRef(constraint)->violated();
}
int kiwi_constraint_expression(
KiwiConstraintRef constraint,
KiwiExpression* out,
int out_size
) {
const auto& expr = ConstraintRef(constraint).instance().expression();
const auto& terms = expr.terms();
const int n_terms = terms.size();
if (!out || out_size < n_terms)
return n_terms;
auto* p = out->terms;
for (const auto& t : terms) {
*p = KiwiTerm {VariableRef(t.variable()), t.coefficient()};
++p;
}
out->term_count = p - out->terms;
out->constant = expr.constant();
return n_terms;
}
KiwiSolverRef kiwi_solver_new() {
return KiwiSolverRef {new (std::nothrow) Solver()};
}
void kiwi_solver_del(KiwiSolverRef s) {
delete reinterpret_cast<Solver*>(s.impl_);
}
KiwiErr kiwi_solver_add_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) {
return wrap_err([=]() {
reinterpret_cast<Solver*>(s.impl_)->addConstraint(ConstraintRef(constraint));
});
}
KiwiErr kiwi_solver_remove_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) {
return wrap_err([=]() {
reinterpret_cast<Solver*>(s.impl_)->removeConstraint(ConstraintRef(constraint));
});
}
int kiwi_solver_has_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) {
return reinterpret_cast<Solver*>(s.impl_)->hasConstraint(ConstraintRef(constraint));
}
KiwiErr kiwi_solver_add_edit_var(KiwiSolverRef s, KiwiVarRef var, double strength) {
return wrap_err([=]() {
reinterpret_cast<Solver*>(s.impl_)->addEditVariable(VariableRef(var), strength);
});
}
KiwiErr kiwi_solver_remove_edit_var(KiwiSolverRef s, KiwiVarRef var) {
return wrap_err([=]() {
reinterpret_cast<Solver*>(s.impl_)->removeEditVariable(VariableRef(var));
});
}
int kiwi_solver_has_edit_var(KiwiSolverRef s, KiwiVarRef var) {
return reinterpret_cast<Solver*>(s.impl_)->hasEditVariable(VariableRef(var));
}
KiwiErr kiwi_solver_suggest_value(KiwiSolverRef s, KiwiVarRef var, double value) {
return wrap_err([=]() {
reinterpret_cast<Solver*>(s.impl_)->suggestValue(VariableRef(var), value);
});
}
void kiwi_solver_update_vars(KiwiSolverRef s) {
reinterpret_cast<Solver*>(s.impl_)->updateVariables();
}
void kiwi_solver_reset(KiwiSolverRef s) {
reinterpret_cast<Solver*>(s.impl_)->reset();
}
void kiwi_solver_dump(KiwiSolverRef s) {
reinterpret_cast<Solver*>(s.impl_)->dump();
}
char* kiwi_solver_dumps(KiwiSolverRef s, void* (*alloc)(size_t)) {
const auto val = reinterpret_cast<Solver*>(s.impl_)->dumps();
const auto buf_size = val.size() + 1;
auto* buf = static_cast<char*>(alloc ? alloc(buf_size) : malloc(buf_size));
if (!buf)
return nullptr;
std::memcpy(buf, val.c_str(), val.size() + 1);
return buf;
}
} // extern "C"

118
ckiwi/ckiwi.h Normal file
View File

@@ -0,0 +1,118 @@
#ifndef CKIWI_H_
#define CKIWI_H_
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define KIWI_REF_ISNULL(ref) ((ref).impl_ == NULL)
// LuaJIT start
enum KiwiErrKind {
KiwiErrNone,
KiwiErrUnsatisfiableConstraint = 1,
KiwiErrUnknownConstraint,
KiwiErrDuplicateConstraint,
KiwiErrUnknownEditVariable,
KiwiErrDuplicateEditVariable,
KiwiErrBadRequiredStrength,
KiwiErrInternalSolverError,
KiwiErrAlloc,
KiwiErrUnknown,
};
enum KiwiRelOp { KIWI_OP_LE, KIWI_OP_GE, KIWI_OP_EQ };
typedef struct {
void* private_;
} KiwiVarRef;
typedef struct {
KiwiVarRef var;
double coefficient;
} KiwiTerm;
typedef struct {
double constant;
int term_count;
KiwiTerm terms[1];
} KiwiExpression;
typedef struct {
void* private_;
} KiwiConstraintRef;
typedef struct {
enum KiwiErrKind kind;
char message[64];
} KiwiErr;
typedef struct {
void* impl_;
} KiwiSolverRef;
KiwiVarRef kiwi_var_new(const char* name);
void kiwi_var_del(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(const KiwiExpression* 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);
int kiwi_constraint_violated(KiwiConstraintRef constraint);
int kiwi_constraint_expression(
KiwiConstraintRef constraint,
KiwiExpression* out,
int out_size
);
KiwiErr kiwi_solver_add_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
KiwiErr kiwi_solver_remove_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
int kiwi_solver_has_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
KiwiErr kiwi_solver_add_edit_var(KiwiSolverRef s, KiwiVarRef var, double strength);
KiwiErr kiwi_solver_remove_edit_var(KiwiSolverRef s, KiwiVarRef var);
int kiwi_solver_has_edit_var(KiwiSolverRef s, KiwiVarRef var);
KiwiErr kiwi_solver_suggest_value(KiwiSolverRef s, KiwiVarRef var, double value);
void kiwi_solver_update_vars(KiwiSolverRef s);
void kiwi_solver_reset(KiwiSolverRef s);
void kiwi_solver_dump(KiwiSolverRef s);
char* kiwi_solver_dumps(KiwiSolverRef s, void* (*alloc)(size_t));
KiwiSolverRef kiwi_solver_new();
void kiwi_solver_del(KiwiSolverRef s);
// LuaJIT end
#ifdef __cplusplus
}
#endif
// Local Variables:
// mode: c++
// End:
#endif // CKIWI_H_

793
kiwi.lua Normal file
View File

@@ -0,0 +1,793 @@
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,
KiwiErrUnknown,
};
enum KiwiRelOp { LE, GE, EQ };
typedef struct {
void* private_;
} KiwiVarRef;
typedef struct {
KiwiVarRef var;
double coefficient;
} KiwiTerm;
typedef struct {
double constant;
int term_count;
KiwiTerm terms_[?];
} KiwiExpression;
typedef struct {
void* private_;
} KiwiConstraintRef;
typedef struct {
enum KiwiErrKind kind;
char message[64];
} KiwiErr;
typedef struct {
void* impl_;
} KiwiSolverRef;
KiwiVarRef kiwi_var_new(const char* name);
void kiwi_var_del(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(const KiwiExpression* 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);
int kiwi_constraint_violated(KiwiConstraintRef constraint);
int kiwi_constraint_expression(
KiwiConstraintRef constraint,
KiwiExpression* out,
int out_size
);
KiwiErr kiwi_solver_add_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
KiwiErr kiwi_solver_remove_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
int kiwi_solver_has_constraint(KiwiSolverRef s, KiwiConstraintRef constraint);
KiwiErr kiwi_solver_add_edit_var(KiwiSolverRef s, KiwiVarRef var, double strength);
KiwiErr kiwi_solver_remove_edit_var(KiwiSolverRef s, KiwiVarRef var);
int kiwi_solver_has_edit_var(KiwiSolverRef s, KiwiVarRef var);
KiwiErr kiwi_solver_suggest_value(KiwiSolverRef s, KiwiVarRef var, double value);
void kiwi_solver_update_vars(KiwiSolverRef s);
void kiwi_solver_reset(KiwiSolverRef s);
void kiwi_solver_dump(KiwiSolverRef s);
char* kiwi_solver_dumps(KiwiSolverRef s, void* (*alloc)(size_t));
KiwiSolverRef kiwi_solver_new();
void kiwi_solver_del(KiwiSolverRef s);
void free(void *);
]])
local strformat = string.format
local ffi_cast, ffi_copy, ffi_istype, ffi_new, ffi_sizeof, ffi_string =
ffi.cast, ffi.copy, ffi.istype, ffi.new, ffi.sizeof, 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.
---| '"KiwiErrUnknown"' # An unknown error occurred.
kiwi.ErrKind = ffi.typeof("enum KiwiErrKind") --[[@as kiwi.ErrKind]]
---@class kiwi.KiwiErr: ffi.ctype*
---@field package kind kiwi.ErrKind
---@field package message ffi.cdata*
---@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("KiwiVarRef") --[[@as kiwi.Var]]
kiwi.Var = Var
local Term = ffi.typeof("KiwiTerm") --[[@as kiwi.Term]]
kiwi.Term = Term
local Expression = ffi.typeof("KiwiExpression") --[[@as kiwi.Expression]]
kiwi.Expression = Expression
local Constraint = ffi.typeof("KiwiConstraintRef") --[[@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
---@param expr kiwi.Expression
---@param term kiwi.Term
---@nodiscard
local function add_expr_term(expr, term)
local ret = ffi_new(Expression, expr.term_count + 1, expr.constant, expr.term_count + 1) --[[@as kiwi.Expression]]
ffi_copy(ret.terms_, expr.terms_, ffi_sizeof(expr.terms_, expr.term_count)) ---@diagnostic disable-line: param-type-mismatch
ret.terms_[expr.term_count] = term
return ret
end
---@param constant number
---@param term kiwi.Term
---@nodiscard
local function new_expr_one(constant, term)
local ret = ffi_new(Expression, 1, constant, 1) --[[@as kiwi.Expression]]
ret.terms_[0] = term
return ret
end
---@param constant number
---@param term1 kiwi.Term
---@param term2 kiwi.Term
---@nodiscard
local function new_expr_pair(constant, term1, term2)
local ret = ffi_new(Expression, 2, constant, 2) --[[@as kiwi.Expression]]
ret.terms_[0] = term1
ret.terms_[1] = term2
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,
__gc = ckiwi.kiwi_var_del,
__new = function(_, name)
return ckiwi.kiwi_var_new(name)
end,
__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
local bt = Term(b)
if type(a) == "number" then
return new_expr_one(a, bt)
else
return new_expr_pair(0.0, Term(a), bt)
end
elseif ffi_istype(Term, b) then
return new_expr_pair(0.0, b, Term(a))
elseif ffi_istype(Expression, b) then
return add_expr_term(b, Term(a))
elseif type(b) == "number" then
return new_expr_one(b, Term(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 coefficient number
---@field var 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 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)
end
ffi.metatype(Term, {
__index = Term_cls,
__new = function(_, var, coefficient)
return ffi_new(Term, var, coefficient or 1.0)
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, Term(b))
elseif ffi_istype(Term, 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(Expression, b) then
return add_expr_term(b, a)
elseif type(b) == "number" then
return new_expr_one(b, a)
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_new(Expression, expr.term_count, expr.constant * constant, expr.term_count) --[[@as kiwi.Expression]]
for i = 0, expr.term_count - 1 do
ret.terms_[i] = ffi_new(Term, expr.terms_[i].var, expr.terms_[i].coefficient * constant) --[[@as kiwi.Term]]
end
return ret
end
---@param a kiwi.Expression
---@param b kiwi.Expression
---@nodiscard
local function add_expr_expr(a, b)
local ret = ffi_new(
Expression,
a.term_count + b.term_count,
a.constant + b.constant,
a.term_count + b.term_count
) --[[@as kiwi.Expression]]
ffi_copy(ret.terms_, a.terms_, ffi_sizeof(a.terms_, a.term_count)) ---@diagnostic disable-line: param-type-mismatch
ffi_copy(ret.terms_[a.term_count], b.terms_, ffi_sizeof(b.terms_, b.term_count)) ---@diagnostic disable-line: param-type-mismatch
return ret
end
---@param expr kiwi.Expression
---@param constant number
---@nodiscard
local function new_expr_constant(expr, constant)
local ret = ffi_new(Expression, expr.term_count, constant, expr.term_count) --[[@as kiwi.Expression]]
ffi_copy(ret.terms_, expr.terms_, ffi_sizeof(expr.terms_, expr.term_count)) ---@diagnostic disable-line: param-type-mismatch
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
terms[i + 1] = self.terms_[i]
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)
return ffi_new(Expression, #terms, constant or 0.0, #terms, terms)
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, Term(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)
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]]
ckiwi.kiwi_constraint_expression(self, expr, n)
end
return expr
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)
return Constraint(
new_expr_pair(-(constant or 0.0), Term(left), Term(right, -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)
return Constraint(
new_expr_pair(-(constant or 0.0), Term(left), Term(right, -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)
return Constraint(new_expr_one(-(constant or 0.0), Term(var)), op, strength)
end
local Strength = kiwi.Strength
local REQUIRED = Strength.REQUIRED
ffi.metatype(Constraint, {
__index = Constraint_cls,
__gc = ckiwi.kiwi_constraint_del,
__new = function(_, expr, op, strength)
return ckiwi.kiwi_constraint_new(expr, op or "EQ", strength or REQUIRED)
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
---@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.kind ~= 0 then
error(new_error(err.kind, ffi_string(err.message), solver, item))
end
end
---@class kiwi.Solver: ffi.ctype*
---@overload fun(): kiwi.Solver
local Solver_cls = {
--- 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
--- Test whether a constraint is in the solver.
---@param constraint kiwi.Constraint
---@return boolean
---@nodiscard
function Solver_cls:has_constraint(constraint)
return ckiwi.kiwi_solver_has_constraint(self, constraint) ~= 0
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
--- Test whether an edit variable has been added to the solver.
---@param var kiwi.Var the edit variable to check
---@return boolean
---@nodiscard
function Solver_cls:has_edit_var(var)
return ckiwi.kiwi_solver_has_edit_var(self, var) ~= 0
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, nil)
local s = ffi_string(cs)
ffi.C.free(cs)
return s
end
kiwi.Solver = ffi.metatype("KiwiSolverRef", {
__index = Solver_cls,
__new = function(_)
return ckiwi.kiwi_solver_new()
end,
__gc = ckiwi.kiwi_solver_del,
__tostring = function(self)
return strformat("kiwi.Solver(0x%X)", ffi_cast("intptr_t", ffi_cast("void*", self)))
end,
}) --[[@as kiwi.Solver]]
return kiwi

33
ljkiwi-scm-1.rockspec Normal file
View File

@@ -0,0 +1,33 @@
rockspec_format = "3.0"
package = "ljkiwi"
version = "scm-1"
source = {
url = "git+https://github.com/jkl1337/ljkiwi",
}
description = {
summary = "A LuaJIT FFI binding for the Kiwi constraint solver.",
detailed = [[
ljkiwi is a LuaJIT FFI binding for the Kiwi constraint solver. Kiwi is a fast
implementation of the Cassowary constraint solving algorithm. ljkiwi provides
reasonably efficient bindings using the LuaJIT FFI.]],
license = "MIT",
issues_url = "https://github.com/jkl1337/ljkiwi/issues",
maintainer = "John Luebs",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "make",
build_variables = {
CFLAGS = "$(CFLAGS)",
LIBFLAG = "$(LIBFLAG)",
LIB_EXT = "$(LIB_EXTENSION)",
OBJ_EXT = "$(OBJ_EXTENSION)",
},
install_variables = {
INST_LIBDIR = "$(LIBDIR)",
INST_LUADIR = "$(LUADIR)",
},
}