Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/text/token.cpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz
src/text/token.cpp 14.3 KB · 442 lines raw
#include <SDL3/SDL.h>

#include "entities/camera.hpp"
#include "entities/world.hpp"
#include "rendering/gl_renderer.hpp"
#include "systems/cpp_highlight_system.hpp"
#include "text/token.hpp"
#include "utils/globals.hpp"

namespace
Nimbin
{
namespace Text
{

static SDL_FColor
aiGlintColor(Token::Ai::State s)
{
    switch (s) {
        case Token::Ai::State::Attack:  return {1.0f, 0.85f, 0.75f, 1.0f};
        case Token::Ai::State::Flee:    return {1.0f, 1.0f,  0.80f, 1.0f};
        case Token::Ai::State::Stunned: return {0.85f, 0.85f, 0.95f, 1.0f};
        case Token::Ai::State::Idle:    return {1.0f, 1.0f,  1.0f,  1.0f};
    }
    return {1.0f, 1.0f, 1.0f, 1.0f};
}

static Vec3 v_norm(const Vec3& v)
{
    double l = SDL_sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
    return (l < 1e-9) ? Vec3{0,0,1} : Vec3{v.x/l, v.y/l, v.z/l};
}

// Camera-facing filled disc (triangle fan)
static void
append_disc(DynArray<float>& v, DynArray<int>& idx,
            const Vec3& c, const Vec3& right, const Vec3& up,
            const Vec3& camN, double radius)
{
    constexpr int SEG = 10;
    const int base = static_cast<int>(v.size()) / 9;

    auto push = [&](const Vec3& p) {
        v.push_back((float)p.x);    v.push_back((float)p.y);    v.push_back((float)p.z);
        v.push_back((float)camN.x); v.push_back((float)camN.y); v.push_back((float)camN.z);
        v.push_back(1.0f);          v.push_back(0.0f);          v.push_back(0.0f);
    };

    push(c);
    for (int i = 0; i < SEG; i++) {
        double ang = static_cast<double>(i) * (2.0 * SDL_PI_D / SEG);
        double dx = SDL_cos(ang) * radius, dy = SDL_sin(ang) * radius;
        push(Vec3{ c.x + right.x*dx + up.x*dy,
                   c.y + right.y*dx + up.y*dy,
                   c.z + right.z*dx + up.z*dy });
    }
    for (int i = 0; i < SEG; i++) {
        idx.push_back(base + 0);
        idx.push_back(base + 1 + i);
        idx.push_back(base + 1 + ((i + 1) % SEG));
    }
}

// Fast integer hash → float in [0,1). Used for all per-sparkle randomness.
static double hash01(uint32_t x)
{
    x ^= x >> 16; x *= 0x7feb352dU;
    x ^= x >> 15; x *= 0x846ca68bU;
    x ^= x >> 16;
    return (x & 0x00ffffffU) / static_cast<double>(0x01000000);
}

static SDL_FColor mix_col(SDL_FColor a, SDL_FColor b, float k)
{
    return { a.r + (b.r - a.r) * k,
             a.g + (b.g - a.g) * k,
             a.b + (b.b - a.b) * k, 1.0f };
}


// A whitespace token's text is a run of spaces or a tab. We keep the real text
// (" " / "\t") for serialization, but build the MESH from an ASCII ghost glyph
// so the token is visible as a tangible thing when it's loose in the world.
// (The mesh path is single-byte, so the ghost must be ASCII — not "·"/"→".)
static bool
text_Is_Whitespace(const std::string& t, bool& has_tab)
{
    if (t.empty()) return false;
    has_tab = false;
    for (char c : t) {
        if      (c == '\t') has_tab = true;
        else if (c != ' ')  return false;
    }
    return true;
}

static std::string
ghost_Glyph(const std::string& t)
{
    return (t.find('\t') != std::string::npos) ? ">" : ".";
}


namespace
Token
{
Data::Data(const std::string& txt,
             float dpth, float scle,
             Vec3 position, SDL_FColor colr)
     : GameObject(position, ObjectType::Text,
                  CollisionShape{}, RigidBody{PhysicsMode::Hover},
                  true, true, true),
       text(txt), color(colr), depth(dpth), scale(scle)
{
    bool has_tab = false;
    if (text_Is_Whitespace(text, has_tab)) {
        is_tab   = has_tab;
        is_space = !has_tab;
    }

    single_color  = Text::Token::get_Color(CppHighlight::classify_Word(text));
    context_color = single_color;   // until a lexer overrides

    if (is_Whitespace()) {
        // Faint glyph that glows when loose in the world (bloomed ghost).
        single_color  = SDL_FColor{ 0.45f, 0.55f, 0.70f, 1.0f };
        context_color = single_color;
        glow_enabled  = true;
        glow_strength = 0.6f;
    }
    // `color` (Static slot) already initialised to `colr` in the member init list.
    rebuild_Mesh();

    body.hover_phase = Random::unit() * SDL_PI_D * 2.0;

    ai.aggressiveness = Random::unit();
    ai.wiggle_phase   = Random::unit() * SDL_PI_D * 2.0;
    ai.wiggle_freq    = 3.0 + Random::unit() * 4.0;
    ai.speed          = 2.5 + Random::unit() * 2.5;
}


Data::~Data()
{
    if (gpu_vbo || gpu_ebo)
        GlRenderer::free_MeshBuffers(gpu_vbo, gpu_ebo);
    if (gpu_vbo_sparkle || gpu_ebo_sparkle)
        GlRenderer::free_MeshBuffers(gpu_vbo_sparkle, gpu_ebo_sparkle);
}


void
Data::set_Color(SDL_FColor c)        // the Static fixed colour
{
    color = c;
}

void
Data::set_Context(SDL_FColor c)      // lexer result inside a document
{
    context_color = c;
    has_context   = true;
}

void
Data::clear_Context()                // leaving a document → fall back to single
{
    has_context = false;
}

void
Data::set_Look(SDL_FColor c, float new_depth)
{
    if      (c.r == -1.0f) single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
    else if (c.g == -1.0f) { /* keep current colours */ }
    else                   color = c;   // explicit colour → Static slot
    set_Depth(new_depth);
}

void
Data::recolor()                       // recompute the tokenized (single) colour
{
    single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
    if (!has_context) context_color = single_color;
}

void
Data::retext(const std::string& s)    // text changed → new single colour + new mesh
{
    if (text == s) return;
    text = s;

    bool has_tab = false;
    const bool ws = text_Is_Whitespace(text, has_tab);
    is_tab   = ws &&  has_tab;
    is_space = ws && !has_tab;

    single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
    if (!has_context) context_color = single_color;
    rebuild_Mesh();
}


void
Data::set_Depth(float new_depth)
{
    depth = new_depth;
    shape.half_extents.z = (double)depth * 0.5;
}


Vec3
Data::words_centroid(const DynArray<TokenPtr>& words)
{
    Vec3   c{};
    double n = 0.0;
    for (const TokenPtr& w : words) {
        if (!w) continue;
        c += w->pos;
        n += 1.0;
    }
    if (n > 0.0) c = c / n;
    return c;
}


Vec3
Data::get_Distance(Vec3 position) const
{
    return pos - position;
}


void
Data::rebuild_Mesh()
{
    // SPIKE("rebuild_mesh");
    // Whitespace keeps its real text (" "/"\t") for serialization, but renders
    // an ASCII ghost glyph so it's a visible thing when loose in the world.
    const std::string build_str = is_Whitespace() ? ghost_Glyph(text) : text;
    loaded = mesh.build(font_path, build_str, 64, depth, scale);
    if (!loaded) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "Text::Token::Data: mesh build failed for \"%s\"", text.c_str());
        return;
    }

    float minX = 1e9f, maxX = -1e9f;
    float minY = 1e9f, maxY = -1e9f;
    for (const Vec3& v : mesh.verts) {
        float vx = static_cast<float>(v.x);
        float vy = static_cast<float>(v.y);
        if (vx < minX) minX = vx;
        if (vx > maxX) maxX = vx;
        if (vy < minY) minY = vy;
        if (vy > maxY) maxY = vy;
    }
    half_extent   = (maxX - minX) * 0.5f;
    half_extent_y = (maxY - minY) * 0.5f;

    shape.half_extents = {
        static_cast<double>(half_extent),
        static_cast<double>(half_extent_y),
        static_cast<double>(depth) * 0.5
    };

    packed_verts.  clear();
    packed_indices.clear();
    const size_t nv = mesh.verts.size();
    packed_verts.reserve(nv * 9);
    for (size_t i = 0; i < nv; i++) {
        const Vec3& v = mesh.verts[i];
        const Vec3& n = mesh.vertex_normals[i];
        const float f = (i < mesh.is_front.size()) ? mesh.is_front[i] : 0.0f;
        packed_verts.push_back((float)v.x); packed_verts.push_back((float)v.y); packed_verts.push_back((float)v.z);
        packed_verts.push_back((float)n.x); packed_verts.push_back((float)n.y); packed_verts.push_back((float)n.z);
        packed_verts.push_back(f);          packed_verts.push_back(0.0f);       packed_verts.push_back(0.0f);
    }
    packed_indices.reserve(mesh.tris.size() * 3);
    for (const MeshTri& t : mesh.tris) {
        packed_indices.push_back(t.a);
        packed_indices.push_back(t.b);
        packed_indices.push_back(t.c);
    }
    gpu_dirty = true;
}


void
Data::update(const Vec3& camera_pos, const double view_mat[9], float carry_dist)
{
    float fx = -static_cast<float>(view_mat[6]);
    float fy = -static_cast<float>(view_mat[7]);
    float fz = -static_cast<float>(view_mat[8]);
    pos.x = camera_pos.x + static_cast<double>(fx * carry_dist);
    pos.y = camera_pos.y + static_cast<double>(fy * carry_dist);
    pos.z = camera_pos.z + static_cast<double>(fz * carry_dist);
    yaw = SDL_atan2f(-fx, -fz);
}


void
Data::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
{
    if (!loaded || packed_indices.empty()) return;

    Mat4 proj      = gl.get_Projection();
    Mat4 view      = buildViewMat4(camera);
    Mat4 model_rot = buildModelMat4(pos, yaw, pitch);

    Mat4 model = model_rot;
    model.m[8]  *= depth;
    model.m[9]  *= depth;
    model.m[10] *= depth;

    Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));

    const SDL_FColor ac = get_ActiveColor();
    gl.set_Wireframe(false);
    gl.set_Light    (light);
    gl.set_MVP      (model, mvp);
    gl.set_NormalMat(model_rot);
    gl.set_FaceShade(ac.r, ac.g, ac.b, back_dim, front_tint);

    if (glow_enabled && glow_strength > 0.0f)
        gl.set_Emissive(ac.r, ac.g, ac.b, glow_strength);
    else
        gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);

    gl.draw_MeshCached(gpu_vbo, gpu_ebo, gpu_index_count, gpu_dirty,
                       packed_verts.data(), (int)packed_verts.size() / 9,
                       packed_indices.data(), (int)packed_indices.size());
    gpu_dirty = false;

    gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
    gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);

    // ── AI sparkle tail ───────────────────────────────────────────────────
    if (ai.state == Ai::State::Idle) return;

    const Uint64 now = SDL_GetTicks();

    // Sample the word's path into a history ring → the trailing tail.
    if (sparkle_trail_count == 0) {
        for (int i = 0; i < SPARKLE_TRAIL; i++) sparkle_trail[i] = pos;
        sparkle_trail_count = SPARKLE_TRAIL;
        sparkle_last_ms     = now;
    }
    else if (now - sparkle_last_ms >= 33) {              // ~30 Hz
        for (int i = SPARKLE_TRAIL - 1; i > 0; --i)
            sparkle_trail[i] = sparkle_trail[i - 1];
        sparkle_trail[0] = pos;
        sparkle_last_ms  = now;
    }

    const double t     = static_cast<double>(now) * 0.001;
    const Vec3   fwd    = v_norm(camera.fwd);
    const Vec3   right  = v_norm(camera.right);
    const Vec3   up     = v_norm(Vec3{ right.y*fwd.z - right.z*fwd.y,
                                       right.z*fwd.x - right.x*fwd.z,
                                       right.x*fwd.y - right.y*fwd.x });
    const Vec3   camN   = { -fwd.x, -fwd.y, -fwd.z };
    const double sz     = SDL_max(half_extent, 0.25);
    const uint32_t wseed = static_cast<uint32_t>(body.hover_phase * 10037.0) | 1u;

    static const SDL_FColor PAL[] = {
        {1.00f, 0.72f, 0.10f, 1.0f},  // gold
        {1.00f, 0.25f, 0.65f, 1.0f},  // magenta-pink
        {0.20f, 0.80f, 1.00f, 1.0f},  // cyan
        {0.45f, 1.00f, 0.30f, 1.0f},  // lime
        {1.00f, 0.45f, 0.10f, 1.0f},  // orange
        {0.65f, 0.40f, 1.00f, 1.0f},  // violet
    };
    constexpr int PALN = sizeof(PAL) / sizeof(PAL[0]);

    DynArray<float> sv[PALN];
    DynArray<int>   si[PALN];

    constexpr int PER_NODE = 2;
    for (int j = 0; j < sparkle_trail_count; ++j) {
        const double age  = (sparkle_trail_count > 1)
                          ? double(j) / double(sparkle_trail_count - 1) : 0.0;
        const Vec3&  node = sparkle_trail[j];

        for (int s = 0; s < PER_NODE; ++s) {
            uint32_t h = wseed ^ (uint32_t(j) * 2654435761u) ^ (uint32_t(s) * 40503u);
            if (hash01(h) < 0.6) continue;               // sparser → calmer

            double phase = hash01(h ^ 0xA5A5u) * 6.28318;
            double tw    = SDL_pow(SDL_max(0.0, SDL_sin(t * 1.6 + phase)), 3.0);  // slower, softer
            if (tw < 0.04) continue;

            double spread = sz * (0.6 + age * 1.4);        // older = wider scatter
            double jx = (hash01(h ^ 0x1111u) - 0.5) * spread;
            double jy = (hash01(h ^ 0x2222u) - 0.5) * spread;

            // gentle independent wander so each sparkle drifts in place
            double wamp = sz * 0.22;                        // how far it roams
            double fx   = 0.6 + hash01(h ^ 0x7777u) * 0.8;  // per-sparkle drift rates
            double fy   = 0.6 + hash01(h ^ 0x8888u) * 0.8;
            jx += SDL_sin(t * fx + hash01(h ^ 0x5555u) * 6.28318) * wamp;
            jy += SDL_cos(t * fy + hash01(h ^ 0x6666u) * 6.28318) * wamp;

            Vec3 ctr = { node.x + right.x*jx + up.x*jy + camN.x*0.03,
                         node.y + right.y*jx + up.y*jy + camN.y*0.03,
                         node.z + right.z*jx + up.z*jy + camN.z*0.03 };

            double size_rng = 0.06 + 0.05 * hash01(h ^ 0x3333u);   // smaller + varied
            double outer    = size_rng * sz * tw * (1.0 - age * 0.6);
            if (outer < 1e-4) continue;

            int c = int(hash01(h ^ 0x4444u) * PALN) % PALN;
            append_disc(sv[c], si[c], ctr, right, up, camN, static_cast<float>(outer));
        }
    }

    Mat4 ident = buildModelMat4(Vec3{0,0,0}, 0.0f, 0.0f);
    gl.set_MVP      (ident, Mat4::mul(proj, view));
    gl.set_NormalMat(ident);

    const SDL_FColor glint = aiGlintColor(ai.state);
    for (int c = 0; c < PALN; ++c) {
        if (si[c].empty()) continue;
        SDL_FColor g = mix_col(PAL[c], glint, 0.15f);     // keep only a hint of state
        gl.set_FaceShade  (0.0f, 0.0f, 0.0f, 1.0f, false);  // no lit term → no white-out
        gl.set_Emissive   (g.r, g.g, g.b, 0.85f);           // pure colour, below clip
        gl.draw_MeshCached(gpu_vbo_sparkle, gpu_ebo_sparkle,
                           gpu_sparkle_index_count, /*dirty=*/true,
                           sv[c].data(), (int)sv[c].size() / 9,
                           si[c].data(), (int)si[c].size());
    }
    gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
    gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);
}


} // namespace Token


} // namespace Text

} // namespace Nimbin