Initial actual commit, awaiting CI
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@
|
|||||||
*.so
|
*.so
|
||||||
*.o
|
*.o
|
||||||
.cache/
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
|||||||
@@ -8,6 +8,5 @@
|
|||||||
"lua_modules/share/lua/5.1/?.lua",
|
"lua_modules/share/lua/5.1/?.lua",
|
||||||
"lua_modules/share/lua/5.1/?/init.lua"
|
"lua_modules/share/lua/5.1/?/init.lua"
|
||||||
],
|
],
|
||||||
"workspace.library": ["lua_modules/share/lua/5.1"],
|
|
||||||
"workspace.checkThirdParty": false
|
"workspace.checkThirdParty": false
|
||||||
}
|
}
|
||||||
|
|||||||
32
Makefile
Normal file
32
Makefile
Normal 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
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
ljkiwi - Free LuaJIT FFI kiwi (Cassowary derived) constraint solver.
|
||||||
|
|
||||||
|
[](https://github.com/jkl1337/ljkiwi/actions/workflows/ci.yml)
|
||||||
|
[](https://coveralls.io/github/jkl1337/ljkiwi?branch=master)
|
||||||
|
[](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
258
ckiwi/ckiwi.cpp
Normal 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
118
ckiwi/ckiwi.h
Normal 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
793
kiwi.lua
Normal 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
33
ljkiwi-scm-1.rockspec
Normal 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)",
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user