sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
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.gzMaybe something with Focus wall or so
assets/sandbox.c | 78 +++++++++++++++++++----------- assets/sandbox.c.last_ok | 78 +++++++++++++++++++----------- src/states/game_state.cpp | 107 +++++++++++++++++++++++++++++++++++++---- src/states/game_state.hpp | 21 +++++++-- src/systems/input_system.cpp | 13 ++--- src/systems/input_system.hpp | 34 +++++++------ src/text/handler.cpp | 110 +++++++++++++++++++++++++++++++------------ src/text/handler.hpp | 44 +++++++++-------- src/text/mesh.cpp | 2 +- src/text/vim_buffer.hpp | 30 ++++++------ 10 files changed, 359 insertions(+), 158 deletions(-) diff --git a/assets/sandbox.c b/assets/sandbox.c index ac2a96b..1c62ace 100644 --- a/assets/sandbox.c +++ b/assets/sandbox.c @@ -1,31 +1,53 @@ -# include "game_mod_abi.h" -static int n ; -static DynObject * o ; -static void box ( double x , double y , double z , double w , double h , double d , int dyn ) { -o [ n ] .kind = DYN_BOX ; o [ n ] .dynamic = dyn ; -double hx = w * 0.5 , hy = h * 0.5 , hz = d * 0.5 ; -o [ n ] .x = x ; -o [ n ] .y = y + hy ; -o [ n ] .z = z ; -o [ n ] .hx = hx ; o [ n ] .hy = hy ; o [ n ] .hz = hz ; -o [ n ] .r = 0.6 ; o [ n ] .g = 0.6 ; o [ n ] .b = 0.8 ; -n = n + 1 ; +#include "game_mod_abi.h" + +static int n; +static DynObject *o; + +static void box(double x, double y, double z, + double w, double h, double d, + int dyn) +{ + o[n].kind = DYN_BOX; + o[n].dynamic = dyn; + + double hx = w * 0.5; + double hy = h * 0.5; + double hz = d * 0.5; + + o[n].x = x; o[n].y = y + hy; o[n].z = z; + o[n].hx = hx; o[n].hy = hy; o[n].hz = hz; + o[n].r = 0.6; o[n].g = 0.6; o[n].b = 0.8; + + n = n + 1; } -static void text ( double x , double y , double z , char * t ) { -o [ n ] .kind = DYN_TEXT ; -o [ n ] .x = x ; o [ n ] .y = y ; o [ n ] .z = z ; -o [ n ] .r = 1 ; o [ n ] .g = 1 ; o [ n ] .b = 1 ; -int i = 0 ; -while ( t [ i ] != 0 ) { o [ n ] .text [ i ] = t [ i ] ; i = i + 1 ; } -o [ n ] .text [ i ] = 0 ; -n = n + 1 ; + +static void text(double x, double y, double z, char *t) +{ + o[n].kind = DYN_TEXT; + + o[n].x = x; o[n].y = y; o[n].z = z; + o[n].r = 1; o[n].g = 1; o[n].b = 1; + int i = 0; + + for (i = 0; t[i] != '\0'; i++) + o[n].text[i] = t[i]; + + o[n].text[i] = 0; + n = n + 1; } -int game_build ( DynObject * out , int max ) { -( void ) max ; -o = out ; n = 0 ; -text ( 0 , - 15 , 5 , "hi" ) ; -box ( 0 , - 16 , 5 , 2 , 1 , 2 , 0 ) ; -for ( int i = 0 ; i < 100 ; i ++ ) -box ( 0 + i , - 16 , 5 + i % 4 , 1 , 1 + i * 0.5 , 1 , 0 ) ; -return n ; + +int game_build(DynObject *out, int max) +{ + (void)max; + + o = out; + n = 0; + + text(0, -15, 5, "hi"); + box(0, -16, 5, 2, 1, 2, 0); + + for (int i = 0; i < 100; i++) + box(0 + i, -16, 5 + i % 4, 1, 1 + i * 0.5, 1, 0); + + return n; } diff --git a/assets/sandbox.c.last_ok b/assets/sandbox.c.last_ok index ac2a96b..1c62ace 100644 --- a/assets/sandbox.c.last_ok +++ b/assets/sandbox.c.last_ok @@ -1,31 +1,53 @@ -# include "game_mod_abi.h" -static int n ; -static DynObject * o ; -static void box ( double x , double y , double z , double w , double h , double d , int dyn ) { -o [ n ] .kind = DYN_BOX ; o [ n ] .dynamic = dyn ; -double hx = w * 0.5 , hy = h * 0.5 , hz = d * 0.5 ; -o [ n ] .x = x ; -o [ n ] .y = y + hy ; -o [ n ] .z = z ; -o [ n ] .hx = hx ; o [ n ] .hy = hy ; o [ n ] .hz = hz ; -o [ n ] .r = 0.6 ; o [ n ] .g = 0.6 ; o [ n ] .b = 0.8 ; -n = n + 1 ; +#include "game_mod_abi.h" + +static int n; +static DynObject *o; + +static void box(double x, double y, double z, + double w, double h, double d, + int dyn) +{ + o[n].kind = DYN_BOX; + o[n].dynamic = dyn; + + double hx = w * 0.5; + double hy = h * 0.5; + double hz = d * 0.5; + + o[n].x = x; o[n].y = y + hy; o[n].z = z; + o[n].hx = hx; o[n].hy = hy; o[n].hz = hz; + o[n].r = 0.6; o[n].g = 0.6; o[n].b = 0.8; + + n = n + 1; } -static void text ( double x , double y , double z , char * t ) { -o [ n ] .kind = DYN_TEXT ; -o [ n ] .x = x ; o [ n ] .y = y ; o [ n ] .z = z ; -o [ n ] .r = 1 ; o [ n ] .g = 1 ; o [ n ] .b = 1 ; -int i = 0 ; -while ( t [ i ] != 0 ) { o [ n ] .text [ i ] = t [ i ] ; i = i + 1 ; } -o [ n ] .text [ i ] = 0 ; -n = n + 1 ; + +static void text(double x, double y, double z, char *t) +{ + o[n].kind = DYN_TEXT; + + o[n].x = x; o[n].y = y; o[n].z = z; + o[n].r = 1; o[n].g = 1; o[n].b = 1; + int i = 0; + + for (i = 0; t[i] != '\0'; i++) + o[n].text[i] = t[i]; + + o[n].text[i] = 0; + n = n + 1; } -int game_build ( DynObject * out , int max ) { -( void ) max ; -o = out ; n = 0 ; -text ( 0 , - 15 , 5 , "hi" ) ; -box ( 0 , - 16 , 5 , 2 , 1 , 2 , 0 ) ; -for ( int i = 0 ; i < 100 ; i ++ ) -box ( 0 + i , - 16 , 5 + i % 4 , 1 , 1 + i * 0.5 , 1 , 0 ) ; -return n ; + +int game_build(DynObject *out, int max) +{ + (void)max; + + o = out; + n = 0; + + text(0, -15, 5, "hi"); + box(0, -16, 5, 2, 1, 2, 0); + + for (int i = 0; i < 100; i++) + box(0 + i, -16, 5 + i % 4, 1, 1 + i * 0.5, 1, 0); + + return n; } diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 3e7ee84..b0d63d7 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -194,6 +194,33 @@ GameState::pult_DispatchAction(PultAction act) void GameState::on_Click() { + if (_focus_active) { + const float ws = World::get_BoxScale<float>(); + const Wall::Transform tf = Wall::get_Transform(_focus_role, ws); + const Vec3 n = Wall::inward_Normal(tf.wall_position); + const Vec3& o = _ctx.camera->pos; + const Vec3& d = _ctx.camera->fwd; + + const double denom = d.x*n.x + d.y*n.y + d.z*n.z; + if (denom < -1e-6) { // crosshair meets the wall + const double t = ((tf.position.x-o.x)*n.x + (tf.position.y-o.y)*n.y + + (tf.position.z-o.z)*n.z) / denom; + if (t > 0.0) { + const Vec3 impact { o.x + d.x*t, o.y + d.y*t, o.z + d.z*t }; + + if (_focus_role == Wall::Role::Editor) { + const Text::Handler::CursorHit hit = + _editor.resolve_Impact(impact); + wallconnect_BeginEditor(); + _editor.place_CursorInsert(hit); + } + else if (_focus_role == Wall::Role::Program) { + wallconnect_BeginProgram(); + } + } + } + return; + } if (_editor.edit_Active()) { const Wall::Transform wall_transform = Wall::get_Transform(Wall::Role::Editor, World::get_BoxScale()); @@ -648,6 +675,7 @@ GameState::update_Input(FrameInput& input, double dt) case InputCommand::Compile: on_Compile(); break; case InputCommand::Run: on_Run(); break; case InputCommand::DebugPrint: on_DebugPrint(); break; + case InputCommand::FocusToggle: _focus_on = !_focus_on; break; case InputCommand::FreelookStart: on_FreelookStart(); break; case InputCommand::FreelookEnd: on_FreelookEnd(); break; case InputCommand::NumberInc: on_NumberAdjust(+1); break; @@ -677,6 +705,7 @@ GameState::update_Input(FrameInput& input, double dt) _editor.end_Edit(); _wall_connection.end(); _frame_input.capture_vim = false; + _focus_locked = false; break; case Text::Handler::EditReq::None: break; @@ -706,14 +735,14 @@ GameState::update_Input(FrameInput& input, double dt) if (input.shoot && _shoot_cooldown <= 0.0) { FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; } - const bool focus = input.focus_wall; + const bool want_focus = _focus_on || _focus_locked; - if (!_freelook_sys->state.active && !focus) + if (!_freelook_sys->state.active && !want_focus) _ctx.player->add_Look(input.look_x, input.look_y); _freelook_sys->update(dt); - if (focus) { + if (want_focus) { const bool f = input.forward, b = input.backward, l = input.left, r = input.right; input.forward = input.backward = input.left = input.right = false; @@ -751,6 +780,7 @@ GameState::update_Objects(double dt, const GlRenderer* gl) if (handler) handler->update(dt, *_ctx.camera); _wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); + focusLabel_Update (dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); DynArray<GameObject*> particle_colliders; particle_colliders.reserve(_ctx.scenery.size()); @@ -875,6 +905,20 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) { FP_ZONE("render.particles" ); _particle_sys ->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.celebration"); _celebration_sys ->draw(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.wall_edit" ); _wall_connection.render(renderer, *_ctx.camera, *_ctx.light); } + { FP_ZONE("render.focus_label"); + if (_focus_active && _focus_label) { + const double hw = static_cast<double>(_focus_label->half_extent); + _focus_label->pos = Vec3{ + _focus_label_pos.x + _ctx.camera->right.x * hw, + _focus_label_pos.y + _ctx.camera->right.y * hw, + _focus_label_pos.z + _ctx.camera->right.z * hw, + }; + _focus_label->yaw = _focus_label_yaw; + _focus_label->pitch = 0.0f; + renderer.clear_Depth(); + _focus_label->draw(renderer, *_ctx.camera, *_ctx.light); + } + } { FP_ZONE("render.end_Frame" ); renderer.end_Frame(&window); } } @@ -1124,11 +1168,13 @@ GameState::sandbox_LoadFileToWall() } DynArray<std::string> lines; - for (const auto& toks : SourceTokens::tokenize_Lines(src)) { - std::string joined; - for (const std::string& tk : toks) { if (!joined.empty()) joined += ' '; joined += tk; } - lines.push_back(joined); + std::string cur; + for (char c : src) { + if (c == '\n') { lines.push_back(cur); cur.clear(); } + else if (c != '\r') { cur += c; } } + if (!cur.empty()) lines.push_back(cur); + if (lines.empty()) lines.push_back(""); _editor.set_Lines(lines); } @@ -1251,6 +1297,7 @@ GameState::on_WallConnectExit() _frame_input.capture_typing = false; _frame_input.typing_buffer.clear(); + _focus_locked = false; } @@ -1342,6 +1389,7 @@ GameState::compute_FocusTarget() _focus_normal = n; _focus_dist = SDL_max(SDL_max(d_v, d_h), 2.0); _focus_yaw_r = tf.yaw; + _focus_role = best_role; return true; } @@ -1349,10 +1397,14 @@ GameState::compute_FocusTarget() void GameState::update_WallFocus(FrameInput& input, double dt) { + const bool want = _focus_on || _focus_locked; constexpr double DUR = 0.25; - if (input.focus_wall) { - if (!_focus_active) { if (!compute_FocusTarget()) return; _focus_active = true; } + if (want) { + if (!_focus_active) { + if (!compute_FocusTarget()) return; + _focus_active = true; + } _focus_t = SDL_min(1.0, _focus_t + dt / DUR); const double ws = World::get_BoxScale(); @@ -1410,3 +1462,40 @@ GameState::update_WallFocus(FrameInput& input, double dt) cam.fwd = { -SDL_sin(yaw_r)*SDL_cos(pitch_r), SDL_sin(pitch_r), -SDL_cos(yaw_r)*SDL_cos(pitch_r) }; cam.right = { SDL_cos(yaw_r), 0.0, -SDL_sin(yaw_r) }; } + + +void +GameState::focusLabel_Update(double dt, const Camera& cam, float aspect, float fov_y) +{ + if (!_focus_active) { _focus_label_init = false; return; } + + if (!_focus_label) + _focus_label = Text::make_Token("focus", Text::Token::depth_wall, 0.0016f, + Vec3{0, 0, 0}, SDL_FColor{0.35f, 0.55f, 1.0f, 1.0f}); + + const Vec3 up = Math::cross(cam.right, cam.fwd); + + const double dist = 2.2; + const double half_h = dist * static_cast<double>(SDL_tanf(fov_y * 0.5f)); + const double half_w = half_h * static_cast<double>(aspect); + + const double fx = 0.82; + const double fy = 0.80; + const double left = half_w * fx; + const double upamt= half_h * fy; + + const Vec3 target { + cam.pos.x + cam.fwd.x*dist - cam.right.x*left + up.x*upamt, + cam.pos.y + cam.fwd.y*dist - cam.right.y*left + up.y*upamt, + cam.pos.z + cam.fwd.z*dist - cam.right.z*left + up.z*upamt, + }; + + if (!_focus_label_init) { _focus_label_pos = target; _focus_label_init = true; } + + const double k = 1.0 - SDL_exp(-6.0 * dt); // exponential trail = "delay in movement" + _focus_label_pos.x += (target.x - _focus_label_pos.x) * k; + _focus_label_pos.y += (target.y - _focus_label_pos.y) * k; + _focus_label_pos.z += (target.z - _focus_label_pos.z) * k; + + _focus_label_yaw = static_cast<float>(SDL_atan2(-cam.fwd.x, -cam.fwd.z)); +} diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 25baeaa..6cb335a 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -83,14 +83,23 @@ class GameState : public IState static constexpr size_t LABEL_HIT_BASE = size_t(1) << 48; // focus wall jump START - Vec3 _focus_center {}; // look-at point on the wall plane (panned by scroll) - Vec3 _focus_normal {}; // inward normal of the focused wall + Vec3 _focus_center {}; // look-at point on the wall plane (panned by scroll) + Vec3 _focus_normal {}; // inward normal of the focused wall + Vec3 _focus_label_pos {}; double _focus_t { 0.0 }; double _focus_dist { 4.0 }; // camera distance from the wall plane (dolly via W/S) double _focus_yaw_r { 0.0 }; float _fov_y { 0.0f }; float _aspect { 0.0f }; + float _focus_label_yaw { 0.0f }; + Text::TokenPtr + _focus_label; bool _focus_active { false }; + bool _focus_locked { false }; + bool _focus_on { false }; + bool _focus_label_init { false }; + Wall::Role + _focus_role { Wall::Role::Editor }; // focus wall jump END void wallconnect_BeginEditor (); @@ -152,9 +161,11 @@ class GameState : public IState // dynamic-lib sandbox END // focus wall jump START - bool compute_FocusTarget(); - void update_WallFocus(FrameInput& input, double dt); - Text::Handler* handler_For(Wall::Role r); + bool compute_FocusTarget(); + void update_WallFocus(FrameInput& input, double dt); + void focusLabel_Update(double dt, const Camera& cam, float aspect, float fov_y); + Text::Handler* + handler_For(Wall::Role r); // focus wall jump END public: diff --git a/src/systems/input_system.cpp b/src/systems/input_system.cpp index 640319a..9758cd3 100644 --- a/src/systems/input_system.cpp +++ b/src/systems/input_system.cpp @@ -11,13 +11,13 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) { switch (e.type) { case SDL_EVENT_MOUSE_MOTION: - input.look_x += static_cast<int>(e.motion.xrel); - input.look_y += static_cast<int>(e.motion.yrel); + input.look_x += (int)e.motion.xrel; + input.look_y += (int)e.motion.yrel; break; case SDL_EVENT_MOUSE_WHEEL: - input.scroll_x += static_cast<double>(e.wheel.x); - input.scroll_y += static_cast<double>(e.wheel.y); + input.scroll_x += (double)e.wheel.x; + input.scroll_y += (double)e.wheel.y; break; case SDL_EVENT_MOUSE_BUTTON_DOWN: { @@ -85,7 +85,9 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDLK_S: input.backward = true; break; case SDLK_D: input.right = true; break; case SDLK_E: input.pick = true; break; - case SDLK_X: input.focus_wall = true; break; + case SDLK_X: if (!e.key.repeat) + input.commands.push_back(InputCommand::FocusToggle); + break; case SDLK_F1: game.change_State(cpputils::make_unique<GameState>()); break; case SDLK_F2: input.commands.push_back(InputCommand::SandboxRestore);break; @@ -118,7 +120,6 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDLK_S: input.backward = false; break; case SDLK_D: input.right = false; break; case SDLK_E: input.pick = false; break; - case SDLK_X: input.focus_wall = false; break; case SDLK_LCTRL: input.normal_keys = false; break; diff --git a/src/systems/input_system.hpp b/src/systems/input_system.hpp index 5116d03..57ee956 100644 --- a/src/systems/input_system.hpp +++ b/src/systems/input_system.hpp @@ -13,6 +13,7 @@ enum class InputCommand : uint8_t { Jump, Click, + FocusToggle, Drop, ShootAll, Compile, @@ -29,24 +30,12 @@ enum class InputCommand : uint8_t struct FrameInput { - DynArray<int> vim_keys; - DynArray<InputCommand> commands; - - std::string typing_buffer; - - double scroll_x = 0.0; - double scroll_y = 0.0; - - int look_x = 0; - int look_y = 0; - - bool forward = false; - bool backward = false; - bool left = false; - bool right = false; - bool pick = false; - bool shoot = false; - bool focus_wall = false; + bool forward = false; + bool backward = false; + bool left = false; + bool right = false; + bool pick = false; + bool shoot = false;// X held: focus the faced wall bool normal_keys = false; @@ -56,6 +45,15 @@ struct FrameInput bool typing_mode = false; bool typing_commit = false; bool typing_cancel = false; + + int look_x = 0; + int look_y = 0; + double scroll_x = 0.0; + double scroll_y = 0.0; + + std::string typing_buffer; + DynArray<int> vim_keys; + DynArray<InputCommand> commands; }; diff --git a/src/text/handler.cpp b/src/text/handler.cpp index 6fc17de..a85ae9a 100644 --- a/src/text/handler.cpp +++ b/src/text/handler.cpp @@ -237,14 +237,14 @@ Handler::detach(Token::Data* t) { if (edit_Active()) return nullptr; - for (size_t li = 0; li < _lines.size(); ++li) { - Line& l = _lines[li]; + for (size_t line_id = 0; line_id < _lines.size(); ++line_id) { + Line& l = _lines[line_id]; for (size_t i = 0; i < l.tokens.size(); ++i) { if (l.tokens[i].get() == t) { TokenPtr out = std::move(l.tokens[i]); l.tokens.erase(l.tokens.begin() + static_cast<std::ptrdiff_t>(i)); if (l.tokens.empty()) - _lines.erase(_lines.begin() + static_cast<std::ptrdiff_t>(li)); + _lines.erase(_lines.begin() + static_cast<std::ptrdiff_t>(line_id)); mark_Dirty(); if (_vim) { sync_VimFromLines(); relayout_Mono(); } // keep buffer in step return out; @@ -306,17 +306,17 @@ Handler::resolve_Target(double y) const std::sort(order.begin(), order.end(), [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; }); - auto glyph_top = [&](size_t li) { - return _lines[li].y - + (double)_lines[li].tokens.front()->half_extent_y + auto glyph_top = [&](size_t line_id) { + return _lines[line_id].y + + (double)_lines[line_id].tokens.front()->half_extent_y + ConfigWall::merge_top_margin; }; - auto underline = [&](size_t li) { return _lines[li].y - ConfigWall::underline_drop; }; + auto underline = [&](size_t line_id) { return _lines[line_id].y - ConfigWall::underline_drop; }; // 1) Merge: within a line's glyph band. - for (size_t li : order) - if (y <= glyph_top(li) && y >= underline(li)) - return { Target::Merge, li, 0.0, 0.0 }; + for (size_t line_id : order) + if (y <= glyph_top(line_id) && y >= underline(line_id)) + return { Target::Merge, line_id, 0.0, 0.0 }; const double LH = ConfigWall::line_height; @@ -774,12 +774,23 @@ Handler::relayout_Mono() const double ws = World::get_BoxScale(); const double bottom_y = -ws + 1.2; - const double anchor_cursor = _eye_y + static_cast<double>(R) * lh; const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh; - // While editing, follow the cursor (but don't let it float above the floor - // alignment). At rest, show the whole file bottom-aligned to the floor. - _row0_y = _capturing ? SDL_min(anchor_cursor, anchor_bottom) - : anchor_bottom; + + if (_capturing) { + // vim-style scrolloff. MARGIN lines are kept above & below the cursor, so the + // cursor stays centred and the text scrolls on every step. PAD leaves a few + // blank rows of overscroll at the file ends so the first/last line sits just + // inside the view instead of flush against (and clipped by) the screen edge. + constexpr int MARGIN = 4; // lines above & below the cursor (lower if rows clip) + constexpr int PAD = 1; // blank rows kept above the first / below the last line + const int visible = 2 * MARGIN + 1; + _scroll_top = SDL_clamp(R - MARGIN, -PAD, SDL_max(0, N - visible) + PAD); + + _row0_y = (_eye_y + static_cast<double>(MARGIN) * lh) + + static_cast<double>(_scroll_top) * lh; + } else { + _row0_y = anchor_bottom; // at rest: whole file bottom-aligned to the floor + } _char_w = mono_CharWidth(); _he_y = mono_HalfExtentY(); @@ -847,18 +858,18 @@ Handler::relayout_Mono() block_code_w = SDL_max(block_code_w, _char_w * (double)vcol); // ← visual width, not s.size() } - size_t ti = 0; + size_t token_id = 0; for (const Chunk& ch : chunks) { TokenPtr t; - if (reuse_line && ti < old[i].tokens.size()) - t = std::move(old[i].tokens[ti]); + if (reuse_line && token_id < old[i].tokens.size()) + t = std::move(old[i].tokens[token_id]); else { t = make_Token(ch.text, Token::depth_wall, 0.003f, _transform.position, ConfigColor::white); t->yaw = _transform.yaw; t->set_ColorSource(ColorSource::Lexed); } br.toks.push_back({ std::move(t), ch.vcol, ch.vspan }); - ++ti; + ++token_id; } built.push_back(std::move(br)); } @@ -1112,7 +1123,6 @@ Handler::begin_Edit(double eye_y) _capturing = true; _eye_y = eye_y; _status.clear(); - _layout = LayoutMode::Mono; relayout_Mono(); mark_Dirty(); @@ -1153,6 +1163,8 @@ Handler::feed_Keys(DynArray<int>& keys, double eye_y) _eye_y = eye_y; if (!_vim) return EditReq::None; + const int row_before = _vim->get_Row(); // detect motion-only changes (j/k/gg/G…) + for (int k : keys) { if (k == -1) _vim->key_enter(); else if (k == -2) _vim->key_escape(); @@ -1180,10 +1192,12 @@ Handler::feed_Keys(DynArray<int>& keys, double eye_y) default: break; } - if (_vim->get_Version() != _vim_seen_version) { + const bool edited = (_vim->get_Version() != _vim_seen_version); + const bool moved = (_vim->get_Row() != row_before); + if (edited || moved) { _vim_seen_version = _vim->get_Version(); - relayout_Mono(); - mark_Dirty(); + relayout_Mono(); // re-anchor for scrolloff (motion) or rebuild (edit) + if (edited) mark_Dirty(); // re-highlight only when text actually changed } return req; } @@ -1226,17 +1240,17 @@ bool Handler::cursor_ToToken(Token::Data* t) { if (!_vim || !t) return false; - for (size_t li = 0; li < _lines.size(); ++li) { - const auto& toks = _lines[li].tokens; + for (size_t line_id = 0; line_id < _lines.size(); ++line_id) { + const auto& toks = _lines[line_id].tokens; size_t col = 0; // byte column of token start - for (size_t ti = 0; ti < toks.size(); ++ti) { - if (toks[ti].get() == t) { - _vim->set_Cursor((int)li, (int)col); + for (size_t token_id = 0; token_id < toks.size(); ++token_id) { + if (toks[token_id].get() == t) { + _vim->set_Cursor((int)line_id, (int)col); _vim_seen_version = _vim->get_Version(); relayout_Mono(); // cursor moved → re-anchor the view return true; } - if (toks[ti]) col += toks[ti]->text.size(); // raw byte length (space/tab = 1) + if (toks[token_id]) col += toks[token_id]->text.size(); // raw byte length (space/tab = 1) } } return false; @@ -1274,6 +1288,44 @@ Handler::cursor_AtImpact(const Vec3& impact) } +Handler::CursorHit +Handler::resolve_Impact(const Vec3& impact) const +{ + CursorHit h; + if (!_vim || _char_w <= 0.0) return h; + const auto& blines = _vim->get_Lines(); + const int N = static_cast<int>(blines.size()); + if (N == 0) return h; + + int row = static_cast<int>(SDL_lround((_row0_y - impact.y) / _line_height)); + row = SDL_clamp(row, 0, N - 1); + + int vc = static_cast<int>(SDL_lround((_code_left_z - impact.z) / _char_w)); + if (vc < 0) vc = 0; + + const std::string& s = blines[static_cast<size_t>(row)]; + int byte = 0, vcol = 0; + while (byte < static_cast<int>(s.size()) && vcol < vc) { + vcol += (s[(size_t)byte] == '\t') ? (4 - (vcol % 4)) : 1; + ++byte; + } + + h.row = row; h.byte = byte; h.ok = true; + return h; +} + + +void +Handler::place_CursorInsert(const CursorHit& hit) +{ + if (!_vim || !hit.ok) return; + _vim->enter_Insert(); // insert mode so the cursor can sit at end-of-line + _vim->set_Cursor(hit.row, hit.byte); + _vim_seen_version = _vim->get_Version(); + relayout_Mono(); // re-anchor: scrolls this cursor into the player's view +} + + VimBuffer::Mode Handler::cursor_Mode() const { return _vim ? _vim->get_Mode() : VimBuffer::Mode::Normal; } diff --git a/src/text/handler.hpp b/src/text/handler.hpp index 34738ce..da8bab4 100644 --- a/src/text/handler.hpp +++ b/src/text/handler.hpp @@ -65,9 +65,11 @@ public: TokenPtr detach (Token::Data* t); void attach (DynArray<TokenPtr> tokens, const Vec3& impact); - - - + // Spatial classifier for thrown words on the always-mono editor grid. + // 80/20 rule: a non-whitespace token's central 80% (of its column span) is a + // hit → DropToken; the outer 20% inserts beside it; whitespace cells always + // insert; vertical gaps between rows create a new line (row may be negative + // to prepend above the top, or >= line count to append below). enum class HitKind : uint8_t { Insert, NewLine, DropToken }; struct EditorHit { HitKind kind; int row; int col; size_t line_id; size_t token_id; }; EditorHit classify_Impact(const Vec3& impact) const; @@ -89,11 +91,11 @@ public: void scroll (double delta); - + // ── vim cursor support (Mono) ────────────────────────────────────────── MonoMetrics mono_Metrics() const; double row_Y (int row) const; - + // ── live editing (Mono) ─────────────────────────────────────────────── enum class EditReq : uint8_t { None, Write, WriteQuit, Exit, Discard }; void begin_Edit (double eye_y); @@ -102,19 +104,22 @@ public: EditReq feed_Keys (DynArray<int>& keys, double eye_y); void mark_Saved (); - int cursor_Row () const; - int cursor_Col () const; - int cursor_VisualCol() const; - bool cursor_ToToken (Token::Data* t); - bool cursor_AtImpact (const Vec3& impact); + int cursor_Row () const; + int cursor_Col () const; + int cursor_VisualCol() const; + bool cursor_ToToken (Token::Data* t); // click-to-place: move cursor to a clicked wall token + bool cursor_AtImpact(const Vec3& impact); // click-to-place: exact char under the crosshair + struct CursorHit { int row = 0; int byte = 0; bool ok = false; }; + CursorHit resolve_Impact (const Vec3& impact) const; // map crosshair → char, current layout + void place_CursorInsert(const CursorHit& hit); // apply after begin_Edit + scroll in const std::string& cmdline_Text() const; std::string modeline () const; VimBuffer::Mode cursor_Mode () const; const DynArray<std::string>& status_Lines() const; - + // ── billboard label ─────────────────────────────────────────────────── void set_Text (const std::string& text); void set_Text (const std::string& text, SDL_FColor color, float depth, float scale); void set_Centre (const Vec3& p); @@ -152,7 +157,7 @@ private: DynArray<std::string> _status; const DynArray<TokenPtr>* _borrowed { nullptr }; - size_t _view_tag { 0 }; + size_t _view_tag { 0 }; // identity of currently-materialised info-wall content; 0 = none bool _lex_enabled { true }; bool _show_numbers { false }; @@ -160,21 +165,22 @@ private: bool _bordered { false }; bool _underline { false }; - + // lexing bool _dirty { false }; uint64_t _gen { 0 }; std::string _last_src; - + // Mono grid cache (vim layout + cursor) double _char_w { 0.0 }; double _he_y { 0.0 }; double _code_left_z{ 0.0 }; double _row0_y { 0.0 }; double _eye_y { 0.0 }; + int _scroll_top { 0 }; // index of the row stuck to the top of the window cpputils::unique_ptr<VimBuffer> _vim; unsigned _vim_seen_version { 0 }; - bool _capturing { false }; + bool _capturing { false }; // keys are being captured (connected & editing) enum class PositionMode : uint8_t { Absolute, Anchor }; const Vec3* _anchor_ptr { nullptr }; @@ -184,7 +190,7 @@ private: Vec3 _attach_offset {}; Vec3 _centre {}; SDL_FColor _style_color { 1.0f, 1.0f, 0.0f, 1.0f }; - std::string _label_text; + std::string _label_text; PositionMode _pos_mode { PositionMode::Absolute }; DynArray<double> _label_off; @@ -192,13 +198,13 @@ private: void billboard_Update (double dt, const Camera& cam); Vec3 label_Centre () const; - + // layout void relayout (); void relayout_Snapped(); void relayout_Mono (); - void sync_VimFromLines(); + void sync_VimFromLines(); // editor: push _lines content back into the persistent buffer - + // Snapped placement internals struct Target { enum Kind { Merge, Insert } kind; size_t line_index; diff --git a/src/text/mesh.cpp b/src/text/mesh.cpp index 5dfb2b6..773f4d3 100644 --- a/src/text/mesh.cpp +++ b/src/text/mesh.cpp @@ -361,7 +361,7 @@ bool TextMesh::build(const std::string& font_path, const std::string& text, int size, float /*depth*/, float scale) { - SPIKE("mesh.build"); + //SPIKE("mesh.build"); verts.clear(); tris.clear(); is_front.clear(); vertex_normals.clear(); FT_Face face = acquire_Face(font_path, size); diff --git a/src/text/vim_buffer.hpp b/src/text/vim_buffer.hpp index be93dba..37fda83 100644 --- a/src/text/vim_buffer.hpp +++ b/src/text/vim_buffer.hpp @@ -37,18 +37,20 @@ public: bool is_Empty() const { return _lines.size() == 1 && _lines[0].empty(); } const std::vector<std::string>& - get_Lines () const { return _lines; } - int get_Row () const { return _row; } - int get_Col () const { return _col; } - Mode get_Mode () const { return _mode; } + get_Lines () const { return _lines; } + int get_Row () const { return _row; } + int get_Col () const { return _col; } + Mode get_Mode () const { return _mode; } unsigned get_Version () const { return _version; } const std::string& - get_Cmdline () const { return _cmd; } + get_Cmdline () const { return _cmd; } const std::string& - get_Status () const { return _status; } + get_Status () const { return _status; } void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); } + void enter_Insert() { _mode = Mode::Insert; clamp_Col(); touch(); } // insert at current cursor + void set_Cursor(int row, int col) { if (_lines.empty()) { _row = 0; _col = 0; return; } @@ -61,14 +63,13 @@ public: private: struct Snapshot { std::vector<std::string> lines; int row; int col; }; - - std::vector<std::string> _saved; - std::vector<std::string> _lines{ "" }; std::vector<Snapshot> _undo; std::vector<Snapshot> _redo; + std::vector<std::string> _saved; + std::vector<std::string> _lines{ "" }; - Mode _mode = Mode::Normal; - Signal _sig = Signal::None; + Mode _mode = Mode::Normal; + Signal _sig = Signal::None; std::string _pending; std::string _cmd; @@ -76,11 +77,10 @@ private: std::string _register; unsigned _version = 0; - int _row = 0, - _col = 0; - int _count = 0; + int _row = 0, _col = 0; + int _count = 0; - bool _dirty = false; + bool _dirty = false; bool _reg_linewise = false; void touch ();