Math Adapters: Host Vector Types​

A math adapter declares that one of the host's own vector, point or color structs is a math::vec3 (or another of the language's math types). That C++ type then crosses every binding as the script's math value, never as a class instance: parameters, return values, bound members, reflected properties and std::vectors of it. Script gets the operators, the math:: library and value semantics; the host keeps its type.

Adapting a type​

One macro per type at global scope, in the header that defines the type, so every translation unit that binds it sees the adapter. It names the type (in parentheses, so template commas pass 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

Member names are the type's own (col, row; R, G, B, A). Each converts with a static_cast to the component type and back, so a double or int member works. JaiScript's own structs, jai::vec2 through jai::color in <jaiscript/math/types.hpp>, are adapted already.

Functions​

An adapted type is an ordinary parameter and return type for add_function and for 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);
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)
C++ signaturewhat crosses
T, const T& parameterthe script vector, converted as a typed slot of that math type converts: a vec2 reaches an ivec2 parameter truncated toward zero. A scalar or a vector of other dimensions finds no overload.
T& parameterthe callee's final value is written back into the script lvalue (a variable, a field, an element, a bound C++ member), as every host T& does (Embedding Quickstart). A temporary is passed as a copy.
T returnarrives as the math type (type_of says math::vec3)
std::vector<T> by value or returnedconverts from and to the packed array<math::vec3> buffer
std::vector<T>&the callee's result is written back
const std::vector<T>&rejected at compile time: it would bind a converted copy

Bound members​

A member bound with .property reads as the math value. Whole-vector, component and compound stores all reach the C++ member; a read is a copy.

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 as its math type with no per-property binding, and every kind of store assigns through the property:

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 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

An observable property notifies on each store that changes the value, compared with the host type's operator== (the component store and the whole store above: two changes). A type without operator== still stores, but never raises on_change.

Values from C++​

make_value makes the math value from a host value, and as<T>() reads one back:

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