Math Adapters: Host Vector Types​

Every engine already has its own vector, point and color structs. A math adapter tells JaiScript that one of them is a math::vec3 (or another of the language's math types): from then on that C++ type crosses every binding as the script's math value, never as a class instance. A parameter, a return value, a bound member, a reflected property and a std::vector of it all exchange the native vector, so script code gets the operators, the math:: library and value semantics, and the host keeps its own type.

Adapting a type​

One line per type, in the header that defines it, so every translation unit that binds the type sees the adapter. The macro names the type (in parentheses, so a template argument list with commas passes through) and the members that hold its components, in order:

C++ (vec_types.h)
#pragma once
#include <jaiscript/math/adapter.hpp>

namespace game {
    struct Vec3 {
        float x = 0, y = 0, z = 0;
        friend bool operator==(const Vec3&, const Vec3&) = default;
    };
    struct Cell { int col = 0, row = 0; };
    struct Rgba { float R = 1, G = 1, B = 1, A = 1; };
}

JAI_BIND_VEC3((game::Vec3), x, y, z)
JAI_BIND_IVEC2((game::Cell), col, row)
JAI_BIND_COLOR((game::Rgba), R, G, B, A)
MacroScript typeComponent members
JAI_BIND_VEC2(T, x, y)math::vec2two, converted to and from float
JAI_BIND_VEC3(T, x, y, z)math::vec3three, float
JAI_BIND_VEC4(T, x, y, z, w)math::vec4four, float
JAI_BIND_IVEC2(T, x, y)math::ivec2two, converted to and from int32
JAI_BIND_IVEC3(T, x, y, z)math::ivec3three, int32
JAI_BIND_COLOR(T, r, g, b, a)math::colorfour channels, float

The macros live in <jaiscript/math/adapter.hpp> and expand to a specialization of jai::math_adapter, so they go at global scope, after the type's definition. The member names are whatever the type uses (col, row; R, G, B, A), and each member converts with a static_cast to the component type and back, so a double or int member works too. JaiScript's own C++ structs, jai::vec2 through jai::color in <jaiscript/math/types.hpp>, are adapted already.

Functions​

With the adapter in scope, an adapted type is an ordinary parameter and return type for add_function, dynamic_binder methods and constructors:

C++
using game::Vec3;
using game::Cell;
using game::Rgba;

eng->add_function("scale", [](Vec3 p, float k) { return Vec3{p.x * k, p.y * k, p.z * k}; });
eng->add_function("nudge", [](Vec3& p) { p.x += 100; });          // written back into the script's vector
eng->add_function("neighbor", [](Cell c) { return Cell{c.col + 1, c.row}; });
eng->add_function("faded", [](Rgba c) { return Rgba{c.R, c.G, c.B, c.A * 0.5f}; });
eng->add_function("make_path", [](int n) {
    std::vector<Vec3> out;
    for (int i = 0; i < n; ++i) { out.push_back(Vec3{float(i), float(i) * 2, 1}); }
    return out;                                                     // arrives as array<math::vec3>
});
eng->add_function("path_length", [](std::vector<Vec3> path) { return static_cast<int64_t>(path.size()); });

jai::dynamic_binder<Body>(*eng, "Body")
    .property("pos", &Body::pos)
    .property("tint", &Body::tint)
    .property("path", &Body::path)
    .method("moved", &Body::moved)
    .build();
jaiscript (with the host above)
var p = scale(math::vec3(1, 2, 3), 2);
print(p, " ", type_of(p));

math::vec3 v = math::vec3(1, 2, 3);
nudge(v);
print(v);

print(neighbor(math::ivec2(4, 7)), " ", neighbor(math::vec2(4.9, 7.2)));   // a vec2 converts like a typed slot
print(faded(math::color(1, 0, 0)));

var path = make_path(3);
print(path.join(" "), " ", type_of(path[0]));
array<math::vec3> pts = [math::vec3(1, 2, 3), math::vec3(4, 5, 6)];
print(path_length(pts));

try { scale(5, 2); } catch (e) { print(e); }
output
(2.0, 4.0, 6.0) math::vec3
(101.0, 2.0, 3.0)
(5, 7) (5, 7)
(1.0, 0.0, 0.0, 0.5)
(0.0, 0.0, 1.0) (1.0, 2.0, 1.0) (2.0, 4.0, 1.0) math::vec3
2
No overload of 'scale' takes (int, int)

Read it back:

  • A by-value or const& parameter receives the script vector as the host type; a returned host value arrives as the math type (type_of says math::vec3).
  • A non-const T& parameter writes the callee's final value back into the script lvalue it was given (a variable, a field, an element, a bound C++ member), the same write-back every host T& parameter makes (Embedding Quickstart). A temporary is passed as a copy.
  • An argument goes through the typed-slot conversion of the parameter's math type: a vec2 reaches an ivec2 parameter truncated toward zero. A scalar or a vector of other dimensions finds no candidate.
  • A std::vector of an adapted type converts from and to the packed array<math::vec3> buffer. Take it by value; a const std::vector<T>& parameter is rejected at compile time (it would bind a converted copy), and a non-const std::vector<T>& writes the callee's result back.

Bound members​

A member of an adapted type bound with .property reads as the math value and stores through: a whole-vector store, a component store and a compound assignment all reach the C++ member. A read is a copy, like any member read of a value type:

C++
struct Body {
    game::Vec3 pos;
    game::Rgba tint;
    std::vector<game::Vec3> path;
    game::Vec3 moved(game::Vec3 d) const { return {pos.x + d.x, pos.y + d.y, pos.z + d.z}; }
};
jaiscript (with the host above)
var b = Body();
b.pos = math::vec3(1, 2, 3);
b.pos.x += 4;                          // a component store reaches the C++ member
b.tint.g = 0.5;
print(b.pos, " ", type_of(b.pos), " ", b.tint);
print(b.moved(math::vec3(1, 1, 1)));

math::vec3 copy = b.pos;               // a read is a copy
copy.x = 0;
print(b.pos);

void halve(float& c) { c /= 2; }
halve(b.pos.y);                        // a member's component binds float&
b.path = [math::vec3(1, 2, 3), math::vec3(4, 5, 6)];
print(b.pos, " ", b.path.size());
output
(5.0, 2.0, 3.0) math::vec3 (1.0, 0.5, 1.0, 1.0)
(6.0, 3.0, 4.0)
(5.0, 2.0, 3.0)
(5.0, 1.0, 3.0) 2

Reflected properties​

A JAI_PROPERTY of an adapted type (dynamic_binder) surfaces in script as its math type with no per-property binding. Whole, component and compound stores assign through the property, so an observable property notifies on each store that changes the value:

C++
#include <jaiscript/properties/property_manager.hpp>
#include <jaiscript/properties/observable_property.hpp>
#include <jaiscript/properties/macros.hpp>
#include <jaiscript/properties/property_serialization.hpp>

class Marker : public jai::property_owner<Marker> {
public:
    JAI_PROPERTY((game::Vec3), position);
    JAI_OBSERVABLE_PROPERTY((game::Vec3), velocity);
    JAI_PROPERTY((game::Rgba), tint);
};
C++
jai::dynamic_binder<Marker>(*eng, "Marker").auto_bind().build();

auto marker = std::make_shared<Marker>();
int changes = 0;
auto watch = marker->velocity.on_change.connect([&](const game::Vec3&, const game::Vec3&) { ++changes; });
eng->add_global("marker", eng->make_object(marker));

eng->execute(R"(
    print(type_of(marker.position), " ", marker.tint);
    marker.position = math::vec3(1, 2, 3);
    marker.position.x += 4;
    marker.velocity.y = 2;
    marker.velocity = math::vec3(1, 1, 1);
    marker.tint.a = 0.5;
)");
std::cout << "position " << marker->position->x << " " << marker->position->y << " " << marker->position->z << "\n";
std::cout << "velocity changes " << changes << ", tint alpha " << marker->tint->A << "\n";
output
math::vec3 (1.0, 1.0, 1.0, 1.0)
position 5 2 3
velocity changes 2, tint alpha 0.5

Change detection compares the old and new value with the host type's operator==. That is why Vec3 in vec_types.h declares one: an observable property of a type without operator== still stores, but silently, with no on_change. Here the component store velocity.y = 2 and the whole store each changed the value, so the receiver ran twice.

Values from C++​

make_value turns a host value into the script math value, and as<T>() reads a math value back as the host type:

C++
jai::script_value v = eng->make_value(game::Vec3{1, 2, 3});         // a math::vec3 value
eng->add_global("spawn_point", v);
game::Vec3 back = eng->execute("spawn_point * 2").as<game::Vec3>();  // and back to the host type
std::cout << v.to_string() << " -> " << back.x << " " << back.y << " " << back.z << "\n";
output
(1.0, 2.0, 3.0) -> 2 4 6