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.gzRestructure Text With Claude
src/entities/pult.hpp | 4 +- src/levels/director.cpp | 26 +- src/states/game_context.hpp | 6 + src/states/game_state.cpp | 533 ++++++++++++++++---------------------- src/states/game_state.hpp | 44 ++-- src/systems/aim.hpp | 8 + src/systems/compiler_system.cpp | 36 +-- src/systems/compiler_system.hpp | 7 +- src/systems/dyn_loader_system.cpp | 51 +--- src/systems/dyn_loader_system.hpp | 11 +- src/systems/hint_system.hpp | 8 +- src/systems/physics_system.cpp | 6 +- src/systems/physics_system.hpp | 4 +- src/systems/pick_system.cpp | 35 ++- src/systems/pick_system.hpp | 15 +- src/systems/program_system.cpp | 89 ++----- src/systems/program_system.hpp | 12 +- src/systems/pult_system.cpp | 6 +- src/systems/pult_system.hpp | 5 +- src/systems/wall_connection.cpp | 314 +++------------------- src/systems/wall_connection.hpp | 59 ++--- src/text/handler.cpp | 260 +++++++++++++++++-- src/text/handler.hpp | 43 ++- src/text/text.cpp | 0 src/text/text.hpp | 0 src/text/token.hpp | 5 + src/text_3d/text_3d.cpp | 351 ------------------------- src/text_3d/text_3d.hpp | 123 ++------- src/text_3d/text_label.cpp | 260 ------------------- src/text_3d/text_label.hpp | 71 ----- src/text_3d/vim_buffer.cpp | 228 ---------------- src/text_3d/vim_buffer.hpp | 76 ------ src/utils/globals.hpp | 8 +- src/utils/text.cpp | 26 +- src/utils/text.hpp | 18 +- src/wall/label.cpp | 4 +- src/wall/label.hpp | 29 +++ src/wall/text.cpp | 87 +------ src/wall/wall.hpp | 27 +- 39 files changed, 805 insertions(+), 2090 deletions(-) diff --git a/src/entities/pult.hpp b/src/entities/pult.hpp index 82e4c68..53c93fc 100644 --- a/src/entities/pult.hpp +++ b/src/entities/pult.hpp @@ -3,9 +3,9 @@ #include <string> #include "entities/solid_box.hpp" -#include "text_3d/text_label.hpp" #include "utils/globals.hpp" #include "utils/light.hpp" +#include "text/handler.hpp" class Camera; class GlRenderer; @@ -42,7 +42,7 @@ struct PultButton final : public SolidBox struct Pult : public SolidBox { std::string name; - TextLabel label; + Text::Handler label; DynArray<PultButton*> buttons; diff --git a/src/levels/director.cpp b/src/levels/director.cpp index 99cbf22..4b6b08d 100644 --- a/src/levels/director.cpp +++ b/src/levels/director.cpp @@ -145,31 +145,20 @@ Director::teardown_Scene(GameContext& ctx) void Director::show_Details(GameContext& ctx) const { - ctx.clear_WallRole(Wall::Role::Info); + if (!ctx.info_wall) return; + Text::Handler& info = *ctx.info_wall; + info.clear(); if (_runner->is_Complete()) return; const std::string& details = _runner->get_Current().details; if (details.empty()) return; - Wall::Transform wt = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); - constexpr size_t MAX_INFO_LINE_CHARS = 92; DynArray<std::string> lines = Text::wrap_String(details, MAX_INFO_LINE_CHARS); - const float bottom_y = World::get_FloorY() + 1.0f; - float y = bottom_y + static_cast<float>(lines.size() - 1) * ConfigWall::text_line_spacing; - - for (const std::string& line : lines) { - Vec3 pos = wt.position; - pos.y = y; - y -= ConfigWall::text_line_spacing; - if (line.empty()) continue; - - Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.0025f, pos, ConfigColor::white); - t->yaw = wt.yaw; - ctx.add_WallText(wt.wall_position, Wall::Role::Info, std::move(t)); - } + for (const std::string& line : lines) + if (!line.empty()) info.append_Line(line); } @@ -189,8 +178,9 @@ Director::build_GoalCtx(const GameContext& ctx) const gctx.ai_words_defeated_this_step = _ledger.ai_defeated; gctx.picked_up_this_step = _ledger.picked_up; - if (gctx.compile_ok_this_step) { - std::string src = Wall::Text::extract_Text(ctx.wall_texts, Wall::Role::Editor); + if (gctx.compile_ok_this_step && ctx.editor_wall) { + std::string src; + for (const std::string& l : ctx.editor_wall->as_Lines()) { src += l; src += '\n'; } gctx.editor_normalized = Level::Check::normalize(src); } return gctx; diff --git a/src/states/game_context.hpp b/src/states/game_context.hpp index 4962355..f0b0065 100644 --- a/src/states/game_context.hpp +++ b/src/states/game_context.hpp @@ -8,11 +8,13 @@ #include "wall/wall.hpp" + class Camera; class Player; class World; struct Light; struct PultButton; +namespace Text { class Handler; } struct GameContext @@ -22,6 +24,10 @@ struct GameContext cpputils::unique_ptr<World> world; cpputils::unique_ptr<Light> light; + + Text::Handler* editor_wall { nullptr }; + Text::Handler* info_wall { nullptr }; + DynArray<Text3dPtr> words; DynArray<GameObjectPtr> scenery; DynArray<PultButton*> buttons; diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index b3002da..6db4cf1 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -15,30 +15,13 @@ #include "systems/physics_system.hpp" #include "systems/pick_system.hpp" #include "systems/source_tokens.hpp" -#include "text_3d/text_label.hpp" #include "utils/config.hpp" #include "utils/frame_profiler.hpp" -#include "utils/text.hpp" +#include "wall/label.hpp" #include "wall/wall.hpp" -static DynArray<std::string> -normalize_EditorLines(const DynArray<std::string>& lines) -{ - DynArray<std::string> out; - for (const std::string& line : lines) { - if (line.find_first_not_of(' ') == std::string::npos) continue; - std::istringstream in(line); - std::string joined, tk; - while (in >> tk) { if (!joined.empty()) joined += ' '; joined += tk; } - out.push_back(joined); - } - return out; -} - - - GameState::GameState(size_t starting_level) { @@ -64,6 +47,50 @@ GameState::GameState(size_t starting_level) _highlight_sys = cpputils::make_unique<CppHighlightSystem>(); _wall_labels = Wall::Label::generate_All(World::get_BoxScale()); + + _editor = Text::Handler(Wall::Role::Editor, World::get_BoxScale(), + Text::LayoutMode::Snapped); + _editor.set_ShowNumbers(true, true); + + _text_walls = { &_editor }; + + _program = Text::Handler(Wall::Role::Program, World::get_BoxScale(), + Text::LayoutMode::Snapped); + _program.set_Lexing(false); + _program.set_ShowNumbers(false, false); + _program.set_DefaultStyle(Text3d::depth_wall, 0.003f, + static_cast<double>(ProgramSystem::line_height), + ConfigWall::free_space_dist); + _program.set_AppendBaseline(static_cast<double>(ProgramSystem::start_y)); + _program.set_FaceMode(Text::FaceMode::Pitch); + + _compiler = Text::Handler(Wall::Role::Compiler, World::get_BoxScale(), + Text::LayoutMode::Snapped); + _compiler.set_Lexing(false); + _compiler.set_ShowNumbers(false, false); + _compiler.set_DefaultStyle(Text3d::depth_compiler, 0.003f, + static_cast<double>(CompilerSystem::line_height), + ConfigWall::free_space_dist); + _compiler.set_AppendBaseline(static_cast<double>(CompilerSystem::start_y)); + _compiler.set_AppendColor(ConfigColor::white); + _compiler.set_FaceMode(Text::FaceMode::Pitch); + + _info = Text::Handler(Wall::Role::Info, World::get_BoxScale(), + Text::LayoutMode::Snapped); + _info.set_Lexing(false); + _info.set_ShowNumbers(false, false); + _info.set_DefaultStyle(Text3d::depth_wall, 0.003f, + static_cast<double>(ConfigWall::text_line_spacing), + ConfigWall::free_space_dist); + _info.set_AppendBaseline(World::get_FloorY<double>() + 1.0); + _info.set_AppendColor(ConfigColor::white); + _info.set_FaceMode(Text::FaceMode::Pitch); + + _text_walls = { &_editor, &_program, &_compiler, &_info }; + + _ctx.editor_wall = &_editor; + _ctx.info_wall = &_info; + _pult_sys = cpputils::make_unique<PultSystem> (); _pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale()); @@ -79,21 +106,6 @@ GameState::GameState(size_t starting_level) if (_level_director->get_Runner().is_DynamicLib()) sandbox_LoadFileToWall(); - - /* - _ctx.scenery.push_back( - cpputils::make_unique<SolidBox>( - Vec3{0, -15, -5}, - Vec3{1.0, 0.5, 1.0} - )); - _ctx.scenery.push_back( - cpputils::make_unique<SolidBox>( - Vec3{3, -15, -5}, - Vec3{1.0, 0.5, 1.0} - )); - _ctx.scenery.back()->body.mode = PhysicsMode::Dynamic; - */ - //debug_SpawnWords(0); } @@ -137,13 +149,11 @@ GameState::carriedline_Update() DynArray<Vec3> slots = _carried.get_SlotPosition(anchor, right, ConfigCarry::gap); DynArray<CarriedWordNode*> nodes = _carried.reverse_nodes(); - // Easing back from the frozen spots: lerp each word from its release - // position toward its live slot. Yaw snaps to live (cheap, reads fine). const bool easing = fs.lerping && !_freelook_return_from.empty(); double s = 1.0; if (easing) { double t = fs.lerp_t; t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t); - s = t * t * (3.0 - 2.0 * t); // smoothstep + s = t * t * (3.0 - 2.0 * t); } for (size_t i = 0; i < nodes.size() && i < slots.size(); ++i) { @@ -182,7 +192,8 @@ GameState::pult_DispatchAction(PultAction act) void GameState::on_Click() { - PickResult r = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _ctx.wall_texts); + PickResult r = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, + _ctx.wall_texts, _text_walls); if (!r.hit()) return; if (r.kind == PickKind::Scenery && r.scenery_obj) { @@ -195,13 +206,22 @@ GameState::on_Click() bool GameState::on_Pick() { - PickResult r = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _ctx.wall_texts); + PickResult r = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, + _ctx.wall_texts, _text_walls); if (!r.hit()) return false; - if (r.kind == PickKind::Wall) { - if (_ctx.wall_texts[r.wall_index] && _ctx.wall_texts[r.wall_index]->text3d) - _editor_wall.on_WordRemoved(_ctx.wall_texts[r.wall_index]->text3d.get()); + if (r.kind == PickKind::WallWord && r.wall_handler && r.wall_token) { + Text3dPtr base = r.wall_handler->detach(r.wall_token); + if (!base) return false; + _level_director->record_PickedUp(base->text); + base->set_Look(ConfigColor::keep, Text3d::depth_floor); + Wall::Text::show_TokenInfo(_info, base->text); + carry_BeginAnim(std::move(base), true); + return true; + } + + if (r.kind == PickKind::Wall) { Text3dPtr base = std::move(_ctx.wall_texts[r.wall_index]->text3d); _ctx.remove_WallText(r.wall_index); if (!base) return false; @@ -209,7 +229,7 @@ GameState::on_Pick() if (base->type == ObjectType::Text) { _level_director->record_PickedUp(base->text); base->set_Look(ConfigColor::keep, Text3d::depth_floor); - Wall::Text::show_TokenInfo(_ctx.wall_texts, base->text); + Wall::Text::show_TokenInfo(_info, base->text); } carry_BeginAnim(std::move(base), true); return true; @@ -222,7 +242,7 @@ GameState::on_Pick() _level_director->record_PickedUp(word->text); word->set_Look(ConfigColor::keep, Text3d::depth_floor); - Wall::Text::show_TokenInfo(_ctx.wall_texts, word->text); + Wall::Text::show_TokenInfo(_info, word->text); carry_BeginAnim(std::move(word), false); return true; @@ -249,7 +269,7 @@ GameState::on_DropLast() _anims_drop.push_back(std::move(drop)); if (!_carried.empty()) - Wall::Text::show_TokenInfo(_ctx.wall_texts, _carried.last->word->text); + Wall::Text::show_TokenInfo(_info, _carried.last->word->text); else _level_director->show_Details(_ctx); @@ -274,12 +294,12 @@ GameState::on_ShootOne() single.push_back(std::move(word)); Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, - _ctx.wall_texts, World::get_BoxScale()); + _ctx.wall_texts, _text_walls, World::get_BoxScale()); _projectiles_sys->launch(_projectiles, std::move(single), _ctx.camera->fwd, _ctx.camera->pos, aim); if (!_carried.empty() && _carried.last && _carried.last->word) - Wall::Text::show_TokenInfo(_ctx.wall_texts, _carried.last->word->text); + Wall::Text::show_TokenInfo(_info, _carried.last->word->text); else _level_director->show_Details(_ctx); _level_director->record_Shot(); @@ -305,7 +325,7 @@ GameState::on_ShootAll() _carried = CarriedLine(); if (!all_words.empty()) { Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery, - _ctx.wall_texts, World::get_BoxScale()); + _ctx.wall_texts, _text_walls, World::get_BoxScale()); _projectiles_sys->launch(_projectiles, std::move(all_words), _ctx.camera->fwd, _ctx.camera->pos, aim); } @@ -315,8 +335,17 @@ GameState::on_ShootAll() } +std::string +GameState::editor_Source() const +{ + std::string src; + for (const std::string& l : _editor.as_Lines()) { src += l; src += '\n'; } + return src; +} + + void GameState::on_Compile() { - std::string src = Wall::Text::extract_Text(_ctx.wall_texts, Wall::Role::Editor); + std::string src = editor_Source(); if ( !_level_director->is_LevelComplete() && _level_director->get_Runner().is_DynamicLib()) { @@ -327,10 +356,10 @@ void GameState::on_Compile() { if (!path.empty() && !stripped.empty()) SourceTokens::write_File(path, src); - _dyn_loader_sys->build(_ctx.wall_texts, src, true); + _dyn_loader_sys->build(_compiler, src, true); } else { - _compiler_sys->compile(_ctx.wall_texts, src); + _compiler_sys->compile(_compiler, src); } } @@ -338,14 +367,14 @@ void GameState::on_Compile() { void GameState::on_Run() { - _program_sys->run(_ctx.wall_texts, World::get_BoxScale()); + _program_sys->run(_program); } void GameState::on_DebugPrint() { - std::print("TEXT::: {0}\n", Wall::Text::extract_Text(_ctx.wall_texts, Wall::Role::Program)); + std::print("TEXT::: {0}\n", editor_Source()); for (Wall::Ptr& wt : _ctx.wall_texts) if (wt->text3d->type == ObjectType::Text) SDL_Log("wall text '%s' y=%.3f", wt->text3d->text.c_str(), wt->text3d->pos.y); @@ -440,7 +469,7 @@ GameState::projectile_FeedProgramInput(ProjectileResult& result) if (!line.empty()) _program_sys->feed_Input(line); - _program_sys->echo_Words(_ctx.wall_texts, result.words, World::get_BoxScale()); + _program_sys->echo_Words(_program, result.words); } @@ -460,61 +489,103 @@ GameState::projectile_LandOnFloor(ProjectileResult& result) void GameState::projectile_LandOnWall(ProjectileResult& result) { - DynArray<Wall::Ptr> placed; - DynArray<Text3d*> editor_group; - placed.reserve(result.words.size()); + DynArray<Text3dPtr> editor_block; + std::string prog_line, comp_line, info_line; for (Text3dPtr& w : result.words) { if (!w) continue; if (w->type == ObjectType::Text) w->set_Look(ConfigColor::keep, Text3d::depth_wall); - w->body.mode = PhysicsMode::Static; w->body.vel = Vec3{0, 0, 0}; Wall::Ptr wt = Wall::Text::from_Drop(std::move(w)); - if (wt && wt->text3d && wt->role == Wall::Role::Editor) - editor_group.push_back(wt->text3d.get()); - placed.push_back(std::move(wt)); + if (!wt || !wt->text3d) continue; + + switch (wt->role) { + case Wall::Role::Editor: + editor_block.push_back(std::move(wt->text3d)); + break; + case Wall::Role::Program: + if (!prog_line.empty()) prog_line += ' '; + prog_line += wt->text3d->text; + break; + case Wall::Role::Compiler: + if (!comp_line.empty()) comp_line += ' '; + comp_line += wt->text3d->text; + break; + case Wall::Role::Info: + if (!info_line.empty()) info_line += ' '; + info_line += wt->text3d->text; + break; + } } - if (!editor_group.empty()) { + if (!editor_block.empty()) { Vec3 impact{0, 0, 0}; - for (Text3d* t : editor_group) + for (const Text3dPtr& t : editor_block) { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; } - const double inv = 1.0 / static_cast<double>(editor_group.size()); + const double inv = 1.0 / static_cast<double>(editor_block.size()); impact.x *= inv; impact.y *= inv; impact.z *= inv; - _editor_wall.on_LinePlaced(editor_group, impact); + _editor.attach(std::move(editor_block), impact); } - - for (Wall::Ptr& wt : placed) - _ctx.wall_texts.push_back(std::move(wt)); - - if (!editor_group.empty()) - editor_SyncFile(); + if (!prog_line.empty()) _program .append_Line(prog_line); + if (!comp_line.empty()) _compiler.append_Line(comp_line); + if (!info_line.empty()) _info .append_Line(info_line); } void GameState::projectile_LandOnWallText(ProjectileResult& result, - DynArray<size_t>& wall_text_to_erase) + DynArray<size_t>& wall_text_to_erase) { - if (Wall::Editor::is_LineNumberHit(result.wall_text_index)) { - const size_t lid = Wall::Editor::line_Of(result.wall_text_index); - const double delta = _editor_wall.scroll_DeltaToEye(lid, _ctx.camera->pos.y); + const size_t idx = result.wall_text_index; - for (Wall::Ptr& wt : _ctx.wall_texts) - if (wt && wt->text3d && wt->role == Wall::Role::Editor) - wt->text3d->pos.y += delta; - _editor_wall.scroll(delta); - _editor_wall.bounce(lid); + if (idx >= LABEL_HIT_BASE) { + for (Text3dPtr& w : result.words) + if (w) _ctx.drop_WordToFloor(std::move(w), result.vel); + return; + } - projectile_LandOnFloor(result); + if (idx >= WALL_TOK_BASE) { + const size_t rel = idx - WALL_TOK_BASE; + const size_t h = rel >> 28; + const size_t li = (rel >> 14) & 0x3FFF; + const size_t ti = rel & 0x3FFF; + Text::Handler* hw = (h < _text_walls.size()) ? _text_walls[h] : nullptr; + + if (hw) { + const auto& lines = hw->get_Lines(); + if (li < lines.size() && ti < lines[li].tokens.size() && lines[li].tokens[ti]) { + if (hw->edit_Active()) { + const Vec3 impact = lines[li].tokens[ti]->pos; + DynArray<Text3dPtr> block; + for (Text3dPtr& w : result.words) if (w) block.push_back(std::move(w)); + hw->attach(std::move(block), impact); + return; + } + if (Text3dPtr orphan = hw->detach(lines[li].tokens[ti].get())) { + orphan->body.hover_height = World::get_FloorY<double>() + 1.5; + orphan->yaw = 0.0f; + _ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0}); + } + } + } + for (Text3dPtr& w : result.words) + if (w) _ctx.drop_WordToFloor(std::move(w), result.vel); + return; } - else { - walltext_HandleShootDown(result, wall_text_to_erase); + + if (idx >= Text::Handler::HIT_BASE) { + const size_t lid = Text::Handler::line_Of(idx); + const double delta = _editor.scroll_DeltaToEye(lid, _ctx.camera->pos.y); + _editor.scroll(delta); + _editor.bounce(lid); + projectile_LandOnFloor(result); + return; } - return; + + walltext_HandleShootDown(result, wall_text_to_erase); } @@ -522,13 +593,9 @@ void GameState::walltext_HandleShootDown(ProjectileResult& result, DynArray<size_t>& to_erase) { - bool removed_editor = false; if (result.wall_text_index < _ctx.wall_texts.size()) { Wall::Ptr& wt = _ctx.wall_texts[result.wall_text_index]; if (wt && wt->text3d) { - removed_editor = (wt->role == Wall::Role::Editor); - _editor_wall.on_WordRemoved(wt->text3d.get()); - Text3dPtr orphan = std::move(wt->text3d); orphan->body.hover_height = World::get_FloorY<double>() + 1.5; orphan->yaw = 0.0f; @@ -541,9 +608,6 @@ GameState::walltext_HandleShootDown(ProjectileResult& result, if (!w) continue; _ctx.drop_WordToFloor(std::move(w), result.vel); } - - if (removed_editor) - editor_SyncFile(); } @@ -557,50 +621,41 @@ void GameState::handle_Input(FrameInput& input, double dt) { for (InputCommand cmd : input.commands) { switch (cmd) { - case InputCommand::Jump: on_Jump(); break; - case InputCommand::Click: on_Click(); break; - case InputCommand::Drop: on_DropLast(); break; - case InputCommand::ShootAll: on_ShootAll(); break; - case InputCommand::Compile: on_Compile(); break; - case InputCommand::Run: on_Run(); break; - case InputCommand::DebugPrint: on_DebugPrint(); break; - case InputCommand::FreelookStart: on_FreelookStart(); break; - case InputCommand::FreelookEnd: on_FreelookEnd(); break; - case InputCommand::NumberInc: on_NumberAdjust(+1); break; - case InputCommand::NumberDec: on_NumberAdjust(-1); break; - case InputCommand::TypingStart: on_TypingStart (input); break; - case InputCommand::TypingCommit: on_TypingCommit(input); break; - case InputCommand::TypingCancel: on_TypingCancel(input); break; - case InputCommand::SandboxRestore: sandbox_Restore(); break; + case InputCommand::Jump: on_Jump(); break; + case InputCommand::Click: on_Click(); break; + case InputCommand::Drop: on_DropLast(); break; + case InputCommand::ShootAll: on_ShootAll(); break; + case InputCommand::Compile: on_Compile(); break; + case InputCommand::Run: on_Run(); break; + case InputCommand::DebugPrint: on_DebugPrint(); break; + case InputCommand::FreelookStart: on_FreelookStart(); break; + case InputCommand::FreelookEnd: on_FreelookEnd(); break; + case InputCommand::NumberInc: on_NumberAdjust(+1); break; + case InputCommand::NumberDec: on_NumberAdjust(-1); break; + case InputCommand::TypingStart: on_TypingStart (input); break; + case InputCommand::TypingCommit: on_TypingCommit(input); break; + case InputCommand::TypingCancel: on_TypingCancel(input); break; + case InputCommand::SandboxRestore: sandbox_Restore(); break; case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break; case InputCommand::ConnectPultExit: on_WallConnectExit(); break; } } if (_wall_connection.get_Target() == Wall::Connection::Target::Editor) { - Wall::Connection::Request req = _wall_connection.feed_Vim(input.vim_keys, _ctx.camera->pos.y); + Text::Handler::EditReq req = _editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y); input.vim_keys.clear(); switch (req) { - case Wall::Connection::Request::Write: - editor_CommitToWall(_wall_connection.get_Lines(), false); - _wall_connection.mark_Saved(); + case Text::Handler::EditReq::Write: + _editor.mark_Saved(); break; - case Wall::Connection::Request::WriteQuit: - editor_CommitToWall(_wall_connection.get_Lines(), true); + case Text::Handler::EditReq::WriteQuit: + case Text::Handler::EditReq::Exit: + case Text::Handler::EditReq::Discard: + _editor.end_Edit(); _wall_connection.end(); _frame_input.capture_vim = false; break; - case Wall::Connection::Request::Exit: - editor_CommitToWall(_wall_connection.get_Lines(), true); - _wall_connection.end(); - _frame_input.capture_vim = false; - break; - case Wall::Connection::Request::QuitDiscard: - editor_CommitToWall(_wall_connection.get_Lines(), true); - _wall_connection.end(); - _frame_input.capture_vim = false; - break; - case Wall::Connection::Request::None: + case Text::Handler::EditReq::None: break; } } @@ -650,7 +705,6 @@ GameState::update(double dt, const GlRenderer* gl) } { FP_ZONE("update.post_step"); - for (Wall::Ptr& wt : _ctx.wall_texts) { if (!wt || !wt->text3d) continue; wt->text3d->pitch = Wall::Text::tilt_TowardEye(wt->text3d->pos, _ctx.camera->pos); @@ -665,7 +719,9 @@ GameState::update(double dt, const GlRenderer* gl) _pult_sys-> update(dt, *_ctx.camera); _plate_sys-> update(dt, *_ctx.player, _level_director->get_StepPlates()); - _editor_wall.update(dt, _ctx.camera->pos); + + for (Text::Handler* h : _text_walls) + if (h) h->update(dt, *_ctx.camera); _wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); } @@ -680,13 +736,13 @@ GameState::update(double dt, const GlRenderer* gl) } { FP_ZONE("update._compiler_sys"); - _compiler_sys->update(_ctx.wall_texts); + _compiler_sys->update(_compiler); if (_compiler_sys->last_compile_ok) { _compiler_sys->last_compile_ok = false; _level_director->record_CompileOk(); } - _dyn_loader_sys->update(_ctx.wall_texts); + _dyn_loader_sys->update(_compiler); if (_dyn_loader_sys->last_build_ok) { _dyn_loader_sys->last_build_ok = false; dyn_Apply(_dyn_loader_sys->load_Objects()); @@ -695,7 +751,7 @@ GameState::update(double dt, const GlRenderer* gl) } { FP_ZONE("update._program_sys"); - _program_sys->update(_ctx.wall_texts, World::get_BoxScale()); + _program_sys->update(_program); } { FP_ZONE("update.goals"); @@ -704,10 +760,13 @@ GameState::update(double dt, const GlRenderer* gl) } { FP_ZONE("update.highlight"); + if (_editor.is_Dirty()) + _highlight_sys->request(_editor.render_Source(), _editor.generation()); + DynArray<HiSpan> spans; uint64_t gen; - if (_highlight_sys->poll(spans, gen) && gen == _editor_gen) - _editor_file.apply_Spans(spans); + if (_highlight_sys->poll(spans, gen)) + _editor.apply_Highlight(spans, gen); } input.look_x = 0; @@ -736,9 +795,9 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) { FP_ZONE("render.world"); _ctx.world ->draw(renderer, *_ctx.camera, *_ctx.light); } - { FP_ZONE("render._editor_wall"); - if (!_editor_hidden) - _editor_wall.draw(renderer, *_ctx.camera, *_ctx.light); + { FP_ZONE("render.text_walls"); + for (Text::Handler* t : _text_walls) + if (t) t->render(renderer, *_ctx.camera, *_ctx.light); } { FP_ZONE("render.scenery"); @@ -752,10 +811,8 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) } { FP_ZONE("render.wall_texts"); - for (const Wall::Ptr& wt : _ctx.wall_texts) { - if (_editor_hidden && wt && wt->role == Wall::Role::Editor) continue; + for (const Wall::Ptr& wt : _ctx.wall_texts) wt->draw(renderer, *_ctx.camera, *_ctx.light); - } } { FP_ZONE("render.wall_labels"); @@ -803,7 +860,7 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) void GameState::infowall_Clear() { - _ctx.clear_WallRole(Wall::Role::Info); + _info.clear(); } @@ -842,10 +899,10 @@ GameState::update_WorldSimulation(double dt) DynArray<ProjectileContactEvent> projectile_contacts; - DynArray<TextLabel*> all_labels = _pult_sys->get_Labels(); + DynArray<Text::Handler*> all_labels = _pult_sys->get_Labels(); { FP_ZONE("updateworldsim.hint_and_labels"); _hint_sys->update(dt, *_ctx.camera); - for (TextLabel* lbl : all_labels) + for (Text::Handler* lbl : all_labels) lbl->update(dt, *_ctx.camera); all_labels.push_back(&_hint_sys->get_Label()); @@ -871,7 +928,25 @@ GameState::update_WorldSimulation(double dt) }); } - for (const Wall::Editor::NumberHit& nh : _editor_wall.number_Hitboxes()) + for (size_t h = 0; h < _text_walls.size(); ++h) { + Text::Handler* hw = _text_walls[h]; + if (!hw) continue; + const auto& elines = hw->get_Lines(); + for (size_t li = 0; li < elines.size(); ++li) { + const DynArray<Text::TokenPtr>& toks = elines[li].tokens; + for (size_t ti = 0; ti < toks.size(); ++ti) { + if (!toks[ti]) continue; + wall_text_hits.push_back({ + Wall::Position::NegX, + WALL_TOK_BASE | (h << 28) | (li << 14) | ti, + toks[ti]->aabb_Min(), + toks[ti]->aabb_Max(), + }); + } + } + } + + for (const Text::Handler::NumberHit& nh : _editor.number_Hitboxes()) wall_text_hits.push_back({ Wall::Position::NegX, nh.index, nh.aabb_Min, nh.aabb_Max }); @@ -879,7 +954,6 @@ GameState::update_WorldSimulation(double dt) const DynArray<Text3dPtr>& words = _wall_labels[li].label.get_Words(); for (size_t wi = 0; wi < words.size(); ++wi) { if (!words[wi]) continue; - constexpr size_t LABEL_HIT_BASE = size_t(1) << 48; wall_text_hits.push_back({ Wall::position_Of(_wall_labels[li].role), LABEL_HIT_BASE | (li << 16) | wi, @@ -1002,7 +1076,6 @@ GameState::dyn_Apply(const DynArray<DynObject>& objs) { dyn_Clear(); - // Build fresh objects from the descriptions. for (const DynObject& o : objs) { if (o.kind == DYN_BOX) { auto box = cpputils::make_unique<SolidBox>( @@ -1014,11 +1087,11 @@ GameState::dyn_Apply(const DynArray<DynObject>& objs) _dyn_scenery.push_back(box.get()); _ctx.scenery.push_back(std::move(box)); } - else { // DYN_TEXT + else { Text3dPtr t = make_Text3d(o.text, Text3d::depth_floor, 0.003f, Vec3{ o.x, o.y, o.z }, SDL_FColor{ o.r, o.g, o.b, 1.0f }); - t->body.mode = PhysicsMode::Static; // lib text just sits where placed + t->body.mode = PhysicsMode::Static; _dyn_scenery.push_back(t.get()); _ctx.words.push_back(std::move(t)); @@ -1049,8 +1122,6 @@ GameState::sandbox_LoadFileToWall() return; } - editor_Clear(); - DynArray<std::string> lines; for (const auto& toks : SourceTokens::tokenize_Lines(src)) { std::string joined; @@ -1058,7 +1129,7 @@ GameState::sandbox_LoadFileToWall() lines.push_back(joined); } - editor_PlaceLines(lines); + _editor.set_Lines(lines); } @@ -1140,15 +1211,12 @@ GameState::wallconnect_BeginProgram() void GameState::wallconnect_BeginEditor() { - DynArray<std::string> lines = editor_ReadLines(); - _editor_absorbed = normalize_EditorLines(lines); - - _editor_hidden = true; + _editor.begin_Edit(_ctx.camera->pos.y); _frame_input.capture_vim = true; _frame_input.vim_keys.clear(); - _wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), lines, _ctx.camera->pos.y); + _wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), _editor); } @@ -1164,12 +1232,12 @@ GameState::on_WallConnectSubmit() for (const char* h : { ":exit leave the connection", "ctrl+WASD move while connected", "<text> + Enter send a line to the program (stdin)" }) - _program_sys->echo_Input(_ctx.wall_texts, h); + _program_sys->echo_Input(_program, h); return; } _program_sys->feed_Input(line); - _program_sys->echo_Input(_ctx.wall_texts, line); + _program_sys->echo_Input(_program, line); } @@ -1186,165 +1254,12 @@ GameState::on_WallConnectExit() void GameState::on_EditorClear() { - Wall::Text::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor); - _editor_wall.clear(); + _editor.clear_ToFloor(_ctx.words); } void GameState::on_EditorScroll(int8_t dt) { - if (dt > 0) { - Wall::Text::scroll_Up(_ctx.wall_texts, Wall::Role::Editor); - _editor_wall.scroll(ConfigWall::scroll_step); - } - else { - Wall::Text::scroll_Down(_ctx.wall_texts, Wall::Role::Editor); - _editor_wall.scroll(-ConfigWall::scroll_step); - } -} - -void -GameState::editor_Clear() -{ - _editor_wall.clear(); - _ctx.clear_WallRole(Wall::Role::Editor); -} - - -DynArray<std::string> -GameState::editor_AbsorbLines() -{ - std::string src = Wall::Text::extract_Text(_ctx.wall_texts, Wall::Role::Editor); - DynArray<std::string> lines; - - std::string cur; - for (char ch : src) { - if (ch == '\n') { lines.push_back(cur); cur.clear(); } - else { cur.push_back(ch); } - } - if (!cur.empty()) lines.push_back(cur); - - editor_Clear(); - return lines; -} - - -DynArray<std::string> -GameState::editor_ReadLines() const -{ - std::string src = Wall::Text::extract_Text(_ctx.wall_texts, Wall::Role::Editor); - DynArray<std::string> lines; - std::string cur; - for (char ch : src) { - if (ch == '\n') { lines.push_back(cur); cur.clear(); } - else { cur.push_back(ch); } - } - if (!cur.empty()) lines.push_back(cur); - return lines; -} - - -void -GameState::editor_PlaceLines(const DynArray<std::string>& lines) -{ - const float ws = World::get_BoxScale(); - const double x_wall = -static_cast<double>(ws) + 0.05; - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - - const double lh = ConfigWall::line_height; - const double gap = 0.5; - const double floor_y = World::get_FloorY<double>(); - - size_t n_lines = 0; - for (const std::string& l : lines) - if (l.find_first_not_of(' ') != std::string::npos) n_lines++; - if (n_lines == 0) return; - - const double bottom_y = floor_y + 1.2; - double y = bottom_y + static_cast<double>(n_lines - 1) * lh; - - struct Tok { Text3dPtr t; double half; }; - struct Row { DynArray<Tok> toks; double width; double y; }; - DynArray<Row> rows; - - double max_width = 0.0; - for (const std::string& line : lines) { - if (line.find_first_not_of(' ') == std::string::npos) - continue; - - Row row; - row.y = y; - double total_w = 0.0; - - std::istringstream in(line); - std::string tk; - while (in >> tk) { - Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f, - Vec3{ x_wall, y, 0.0 }, ConfigColor::wall); - t->yaw = tf.yaw; - const double half = static_cast<double>(t->half_extent); - total_w += 2.0 * half; - row.toks.push_back({ std::move(t), half }); - } - if (row.toks.empty()) { y -= lh; continue; } - total_w += gap * static_cast<double>(row.toks.size() - 1); - row.width = total_w; - max_width = SDL_max(max_width, total_w); - - rows.push_back(std::move(row)); - y -= lh; - } - if (rows.empty()) return; - - const double left_z = max_width * 0.5; - - for (Row& row : rows) { - DynArray<Text3d*> placed; - double cursor = left_z; - for (Tok& tok : row.toks) { - const double center_z = cursor - tok.half; - tok.t->pos = Vec3{ x_wall, row.y, center_z }; - placed.push_back(tok.t.get()); - _ctx.add_WallText(Wall::Position::NegX, Wall::Role::Editor, std::move(tok.t)); - cursor = center_z - tok.half - gap; - } - _editor_wall.add_PlacedLine(placed, row.y); - } - - editor_SyncFile(); -} - - -void -GameState::editor_CommitToWall(const DynArray<std::string>& lines, bool ending) -{ - DynArray<std::string> incoming = normalize_EditorLines(lines); - - bool same = (incoming.size() == _editor_absorbed.size()); - if (same) - for (size_t i = 0; i < incoming.size(); ++i) - if (incoming[i] != _editor_absorbed[i]) { same = false; break; } - - if (ending) _editor_hidden = false; - - if (same) return; - - editor_Clear(); - editor_PlaceLines(lines); - _editor_absorbed = incoming; -} - -void -GameState::editor_SyncFile() -{ - _editor_file.clear(); - for (const DynArray<Text3d*>& line : _editor_wall.lines_InOrder()) { - Text::Line fl; - for (Text3d* w : line) - if (w) fl.tokens.push_back(Text::Token{ w->text, 0, 0, w }); - _editor_file.lines.push_back(std::move(fl)); - } - - ++_editor_gen; - _highlight_sys->request(_editor_file.render_Source(), _editor_gen); + if (dt > 0) _editor.scroll( ConfigWall::scroll_step); + else _editor.scroll(-ConfigWall::scroll_step); } diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 573abcf..7185ecd 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -24,10 +24,10 @@ #include "systems/wall_connection.hpp" #include "systems/word_ai_system.hpp" #include "systems/word_anim.hpp" -#include "text_3d/vim_buffer.hpp" +#include "text/handler.hpp" // Text::Handler (replaces Wall::Editor + Text::SrcView) +#include "text/vim_buffer.hpp" #include "utils/globals.hpp" -#include "utils/text.hpp" -#include "wall/editor.hpp" +#include "wall/label.hpp" #include "wall/wall.hpp" @@ -55,41 +55,37 @@ class GameState : public IState cpputils::unique_ptr<WordAiSystem> _word_ai_sys; - bool _editor_hidden { false }; - uint64_t _editor_gen { 0 }; double _pick_cooldown { 0.0 }; double _shoot_cooldown { 0.0 }; std::string _typing_last; CarriedLine _carried; - Wall::Editor _editor_wall; + Text::Handler _editor; + Text::Handler _program; + Text::Handler _compiler; + Text::Handler _info; + Wall::Connection _wall_connection; FrameInput _frame_input; GameContext _ctx; - Wall::Connection _wall_connection; - Text::View _editor_file; + DynArray<GameObject*> _dyn_scenery; + DynArray<Text3d*> _typing_words; + DynArray<Vec3> _freelook_return_from; + DynArray<Projectile> _projectiles; + DynArray<Wall::Label> _wall_labels; + DynArray<WordAnim_Add> _anims_add; + DynArray<WordAnim_Drop> _anims_drop; + DynArray<Text::Handler*> _text_walls; - DynArray<GameObject*> _dyn_scenery; - DynArray<Text3d*> _typing_words; - DynArray<Vec3> _freelook_return_from; - DynArray<std::string> _editor_absorbed; - DynArray<Projectile> _projectiles; - DynArray<Wall::Label> _wall_labels; - DynArray<WordAnim_Add> _anims_add; - DynArray<WordAnim_Drop> _anims_drop; + // editor-token projectile-hit id range (line numbers use Text::Handler::HIT_BASE) + static constexpr size_t WALL_TOK_BASE = size_t(1) << 40; + static constexpr size_t LABEL_HIT_BASE = size_t(1) << 48; void wallconnect_BeginEditor (); void wallconnect_BeginProgram(); - DynArray<std::string> - editor_AbsorbLines (); - DynArray<std::string> - editor_ReadLines () const; - void editor_Clear (); - void editor_CommitToWall (const DynArray<std::string>& lines, bool ending); - void editor_PlaceLines (const DynArray<std::string>& lines); - void editor_SyncFile (); + std::string editor_Source() const; bool on_Pick (); diff --git a/src/systems/aim.hpp b/src/systems/aim.hpp index cbb1641..2890238 100644 --- a/src/systems/aim.hpp +++ b/src/systems/aim.hpp @@ -5,6 +5,7 @@ #include "entities/camera.hpp" #include "entities/game_object.hpp" #include "systems/collision.hpp" +#include "text/handler.hpp" #include "text_3d/text_3d.hpp" #include "utils/globals.hpp" #include "wall/wall.hpp" @@ -27,6 +28,7 @@ crosshair_Target(const Camera& camera, const DynArray<Text3dPtr>& words, const DynArray<GameObjectPtr>& scenery, const DynArray<Wall::Ptr>& wall_texts, + const DynArray<Text::Handler*>& walls, float world_scale) { const Vec3 eye = camera.pos; @@ -71,6 +73,12 @@ crosshair_Target(const Camera& camera, for (const Wall::Ptr& wt : wall_texts) if (wt && wt->text3d) test(wt->text3d->aabb_Min(), wt->text3d->aabb_Max(), nullptr); + for (const Text::Handler* h : walls) + if (h) + for (const Text::Line& line : h->get_Lines()) + for (const Text::TokenPtr& tp : line.tokens) + if (tp) test(tp->aabb_Min(), tp->aabb_Max(), nullptr); + if (win) { out.track = win; const double dx = out.point.x - win->pos.x; diff --git a/src/systems/compiler_system.cpp b/src/systems/compiler_system.cpp index 35d5602..a520713 100644 --- a/src/systems/compiler_system.cpp +++ b/src/systems/compiler_system.cpp @@ -1,8 +1,6 @@ #include <SDL3/SDL.h> -#include <algorithm> #include "systems/compiler_system.hpp" -#include "text_3d/text_3d.hpp" #include "utils/text.hpp" @@ -13,7 +11,7 @@ CompilerSystem::~CompilerSystem() void -CompilerSystem::compile(Wall::PtrList& texts, const std::string& src) +CompilerSystem::compile(Text::Handler& out, const std::string& src) { // One compile at a time. Reap a finished worker; ignore re-triggers while // one is still running. @@ -22,13 +20,7 @@ CompilerSystem::compile(Wall::PtrList& texts, const std::string& src) last_compile_ok = false; - // Clear previous output from the compiler wall (main thread — GL-owned). - texts.erase( - std::remove_if(texts.begin(), texts.end(), - [](const Wall::Ptr& wt) { - return wt->wall_position == Wall::position_Of(Wall::Role::Compiler); - }), - texts.end()); + out.clear(); // wipe previous diagnostics from the compiler wall _count = 0; { @@ -86,7 +78,7 @@ CompilerSystem::run_Compile(CompilerSystem* self, std::string src) void -CompilerSystem::update(Wall::PtrList& texts) +CompilerSystem::update(Text::Handler& out) { // Drain whatever the worker produced and render it (main thread, GL-safe). DynArray<std::string> lines; @@ -97,30 +89,14 @@ CompilerSystem::update(Wall::PtrList& texts) } for (const std::string& line : lines) - push_Line(texts, line); + push_Line(out, line); } void -CompilerSystem::push_Line(Wall::PtrList& texts, const std::string& line) +CompilerSystem::push_Line(Text::Handler& out, const std::string& line) { if (line == "OK") last_compile_ok = true; - - std::string clean = Text::ascii_Sanitize(line); - - for (Wall::Ptr& wt : texts) - if (wt->role == Wall::Role::Compiler) - wt->text3d->pos.y += static_cast<double>(line_height); - - Wall::Transform tf = Wall::get_Transform(Wall::Role::Compiler, World::get_BoxScale()); - - Vec3 pos = tf.position; - pos.y = start_y; - - Text3dPtr t = make_Text3d(clean, Text3d::depth_compiler, 0.003f, pos, ConfigColor::white); - t->yaw = tf.yaw; - - texts.push_back(cpputils::make_unique<Wall::Text>(Wall::Text{ - tf.wall_position, Wall::Role::Compiler, std::move(t) })); + out.append_Line(Text::ascii_Sanitize(line)); _count++; } diff --git a/src/systems/compiler_system.hpp b/src/systems/compiler_system.hpp index e835fd0..0f1df7a 100644 --- a/src/systems/compiler_system.hpp +++ b/src/systems/compiler_system.hpp @@ -7,6 +7,7 @@ #include <thread> #include "entities/world.hpp" +#include "text/handler.hpp" #include "utils/globals.hpp" @@ -19,7 +20,7 @@ private: std::thread _worker; DynArray<std::string> _pending; - void push_Line (Wall::PtrList&, const std::string& line); + void push_Line (Text::Handler& out, const std::string& line); static void run_Compile(CompilerSystem* self, std::string src); public: static inline float line_height = 0.4f; @@ -27,8 +28,8 @@ public: bool last_compile_ok{false}; - void update (Wall::PtrList&); - void compile (Wall::PtrList&, const std::string& src); + void update (Text::Handler& out); + void compile (Text::Handler& out, const std::string& src); bool is_Running() const { return _running.load(); } diff --git a/src/systems/dyn_loader_system.cpp b/src/systems/dyn_loader_system.cpp index 8109e3a..f9fb0c7 100644 --- a/src/systems/dyn_loader_system.cpp +++ b/src/systems/dyn_loader_system.cpp @@ -1,18 +1,13 @@ #include <SDL3/SDL.h> -#include <algorithm> #include <dlfcn.h> #include <string> #include <sys/select.h> -#include "entities/world.hpp" #include "systems/dyn_loader_system.hpp" -#include "text_3d/text_3d.hpp" #include "utils/text.hpp" // Where game_mod_abi.h is installed at runtime, and the build scratch dir. -// The header must be reachable by the runtime gcc via this -I path. Adjust to -// wherever your CMake installs it (see CMake notes). static constexpr const char* MOD_INCLUDE_DIR = "/tmp/game_mod_include"; static constexpr const char* MOD_SRC_PATH = "/tmp/game_mod.c"; @@ -64,17 +59,11 @@ DynLoaderSystem::wrap_Source(const std::string& user_body) void -DynLoaderSystem::build(Wall::PtrList& texts, const std::string& user_src, bool full_source) +DynLoaderSystem::build(Text::Handler& out, const std::string& user_src, bool full_source) { last_build_ok = false; - // Clear previous output from the Compiler wall. - texts.erase( - std::remove_if(texts.begin(), texts.end(), - [](const Wall::Ptr& wt) { - return wt->wall_position == Wall::position_Of(Wall::Role::Compiler); - }), - texts.end()); + out.clear(); // wipe previous diagnostics from the compiler wall _count = 0; // Write wrapped source. @@ -82,11 +71,11 @@ DynLoaderSystem::build(Wall::PtrList& texts, const std::string& user_src, bool f if (!f) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "DynLoaderSystem: cannot write %s", MOD_SRC_PATH); - push_Line(texts, "internal error: cannot write source"); + push_Line(out, "internal error: cannot write source"); return; } - const std::string out = full_source ? user_src : wrap_Source(user_src); - fputs(out.c_str(), f); + const std::string src = full_source ? user_src : wrap_Source(user_src); + fputs(src.c_str(), f); fclose(f); // Unique output path each build — dlopen() caches handles by path, so a @@ -110,7 +99,7 @@ DynLoaderSystem::build(Wall::PtrList& texts, const std::string& user_src, bool f void -DynLoaderSystem::update(Wall::PtrList& texts) +DynLoaderSystem::update(Text::Handler& out) { if (!_pipe) return; @@ -127,14 +116,14 @@ DynLoaderSystem::update(Wall::PtrList& texts) if (_count == 0) { last_build_ok = true; _last_so_path = _pending_so; - push_Line(texts, "BUILD OK"); + push_Line(out, "BUILD OK"); } return; } std::string line(buf); if (!line.empty() && line.back() == '\n') line.pop_back(); if (!line.empty()) - push_Line(texts, line); + push_Line(out, line); FD_ZERO(&set); FD_SET(fd, &set); } @@ -142,30 +131,13 @@ DynLoaderSystem::update(Wall::PtrList& texts) void -DynLoaderSystem::push_Line(Wall::PtrList& texts, const std::string& line) +DynLoaderSystem::push_Line(Text::Handler& out, const std::string& line) { - // Shift existing compiler lines up, drop the new one at the bottom. - for (Wall::Ptr& wt : texts) - if (wt->role == Wall::Role::Compiler) - wt->text3d->pos.y += static_cast<double>(line_height); - - Wall::Transform tf = Wall::get_Transform(Wall::Role::Compiler, World::get_BoxScale()); - Vec3 pos = tf.position; - pos.y = start_y; - - std::string clean = Text::ascii_Sanitize(line); - Text3dPtr t = make_Text3d(clean, Text3d::depth_compiler, 0.003f, - pos, ConfigColor::compiler); - t->yaw = tf.yaw; - - texts.push_back(cpputils::make_unique<Wall::Text>(Wall::Text{ - tf.wall_position, - Wall::Role::Compiler, - std::move(t) - })); + out.append_Line(Text::ascii_Sanitize(line)); _count++; } + DynArray<DynObject> DynLoaderSystem::load_Objects() { @@ -189,7 +161,6 @@ DynLoaderSystem::load_Objects() } using BuildFn = int(*)(DynObject*, int); - // The cast-through-void* dance silences -Wpedantic on function-pointer casts. BuildFn game_build = reinterpret_cast<BuildFn>(dlsym(_handle, "game_build")); if (!game_build) { diff --git a/src/systems/dyn_loader_system.hpp b/src/systems/dyn_loader_system.hpp index 183e4fa..c4adf16 100644 --- a/src/systems/dyn_loader_system.hpp +++ b/src/systems/dyn_loader_system.hpp @@ -3,15 +3,12 @@ #include <string> #include "systems/game_mod_abi.h" +#include "text/handler.hpp" #include "utils/globals.hpp" -#include "wall/wall.hpp" class DynLoaderSystem { - static constexpr float line_height = 0.25f; - static constexpr float start_y = -15.0f; - FILE* _pipe { nullptr }; void* _handle { nullptr }; @@ -22,15 +19,15 @@ class DynLoaderSystem std::string _last_so_path; - void push_Line (Wall::PtrList& texts, const std::string& line); + void push_Line (Text::Handler& out, const std::string& line); static std::string wrap_Source(const std::string& user_body); public: bool last_build_ok { false }; - void build (Wall::PtrList& texts, const std::string& user_src, bool full_source); - void update (Wall::PtrList& texts); + void build (Text::Handler& out, const std::string& user_src, bool full_source); + void update (Text::Handler& out); DynArray<DynObject> load_Objects(); diff --git a/src/systems/hint_system.hpp b/src/systems/hint_system.hpp index 8876730..c19ece1 100644 --- a/src/systems/hint_system.hpp +++ b/src/systems/hint_system.hpp @@ -1,7 +1,7 @@ #pragma once #include <string> -#include "text_3d/text_label.hpp" +#include "text/handler.hpp" #include "entities/camera.hpp" #include "utils/light.hpp" @@ -12,7 +12,7 @@ class GlRenderer; class HintSystem { std::string _current_hint; - TextLabel _label; + Text::Handler _label; public: HintSystem (); @@ -23,8 +23,8 @@ public: void set_Hint(const std::string& hint_text); - TextLabel& + Text::Handler& get_Label() { return _label; } - const TextLabel& + const Text::Handler& get_Label() const { return _label; } }; diff --git a/src/systems/physics_system.cpp b/src/systems/physics_system.cpp index 4ceb678..d396f0b 100644 --- a/src/systems/physics_system.cpp +++ b/src/systems/physics_system.cpp @@ -6,9 +6,9 @@ #include "systems/physics_system.hpp" #include "systems/projectile_system.hpp" #include "text_3d/text_3d.hpp" -#include "text_3d/text_label.hpp" #include "utils/config.hpp" #include "utils/frame_profiler.hpp" +#include "wall/label.hpp" void @@ -374,7 +374,7 @@ PhysicsSystem::resolve_SingleBoxCollision( void PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, const DynArray<GameObject*>* statics, - const DynArray<TextLabel*>* labels, + const DynArray<Text::Handler*>* labels, DynArray<ProjectileContactEvent>* contacts) { for (Projectile& proj : projectiles) { @@ -436,7 +436,7 @@ PhysicsSystem::resolve_Projectiles(DynArray<Projectile>& projectiles, if (labels) { constexpr double RESTITUTION_LABEL = 0.55; - for (TextLabel* label : *labels) { + for (Text::Handler* label : *labels) { if (!label || label->is_Empty()) continue; const auto& lw = label->get_Words(); diff --git a/src/systems/physics_system.hpp b/src/systems/physics_system.hpp index 9c44789..a5c426c 100644 --- a/src/systems/physics_system.hpp +++ b/src/systems/physics_system.hpp @@ -4,11 +4,11 @@ #include "entities/player.hpp" #include "systems/carry_system.hpp" #include "systems/collision.hpp" +#include "text/handler.hpp" #include "utils/globals.hpp" struct Projectile; -class TextLabel; struct ProjectileContactEvent @@ -54,7 +54,7 @@ public: const DynArray<Text3dPtr>& words); void resolve_Projectiles(DynArray<Projectile>& projectiles, const DynArray<GameObject*>* statics, - const DynArray<TextLabel*>* labels, + const DynArray<Text::Handler*>* labels, DynArray<ProjectileContactEvent>* contacts); void resolve_PlayerVsObjects(Player& player, CarriedLine& carried, diff --git a/src/systems/pick_system.cpp b/src/systems/pick_system.cpp index 8663694..761105d 100644 --- a/src/systems/pick_system.cpp +++ b/src/systems/pick_system.cpp @@ -58,12 +58,12 @@ ang_score(const GameObject& obj, const Vec3& eye, const Vec3& fwd) PickResult pick(const Camera& camera, - const DynArray<Text3dPtr>& words, - const DynArray<GameObjectPtr>& scenery, - const DynArray<Wall::Ptr>& wall_texts) + const DynArray<Text3dPtr>& words, + const DynArray<GameObjectPtr>& scenery, + const DynArray<Wall::Ptr>& wall_texts, + const DynArray<Text::Handler*>& walls) { - const Vec3& eye = camera.pos; - + const Vec3& eye = camera.pos; const double floor_y = World::get_FloorY(); PickResult result; @@ -72,8 +72,7 @@ pick(const Camera& camera, for (size_t i = 0; i < words.size(); ++i) { const Text3dPtr& w = words[i]; if (!w || !w->interactable) continue; - if (w->ai.state != Text3dAI::State::Idle) - continue; + if (w->ai.state != Text3dAI::State::Idle) continue; if (w->pos.y < floor_y) continue; double a = ang_score(*w, eye, camera.fwd); @@ -101,12 +100,30 @@ pick(const Camera& camera, if (wt->text3d->pos.y < floor_y) continue; double a = ang_score(*wt->text3d, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { - best_angle = a; - result.kind = PickKind::Wall; + best_angle = a; + result.kind = PickKind::Wall; result.wall_index = i; } } + for (Text::Handler* h : walls) { + if (!h || h->edit_Active()) continue; + for (const Text::Line& line : h->get_Lines()) { + for (const Text::TokenPtr& tp : line.tokens) { + Text::Token* t = tp.get(); + if (!t || !t->interactable) continue; + if (t->pos.y < floor_y) continue; + double a = ang_score(*t, eye, camera.fwd); + if (a >= 0.0 && a < best_angle) { + best_angle = a; + result.kind = PickKind::WallWord; + result.wall_handler = h; + result.wall_token = t; + } + } + } + } + return result; } diff --git a/src/systems/pick_system.hpp b/src/systems/pick_system.hpp index fc31e4a..5a94f0c 100644 --- a/src/systems/pick_system.hpp +++ b/src/systems/pick_system.hpp @@ -2,6 +2,7 @@ #include "entities/camera.hpp" #include "entities/game_object.hpp" +#include "text/handler.hpp" #include "text_3d/text_3d.hpp" #include "utils/globals.hpp" #include "wall/wall.hpp" @@ -13,6 +14,7 @@ enum class PickKind : uint8_t Scenery, Wall, Word, + WallWord, }; @@ -21,8 +23,10 @@ struct PickResult size_t word_index { 0 }; size_t wall_index { 0 }; - GameObject* scenery_obj { nullptr }; - PickKind kind { PickKind::None }; + GameObject* scenery_obj { nullptr }; + Text::Handler* wall_handler { nullptr }; + Text::Token* wall_token { nullptr }; + PickKind kind { PickKind::None }; bool hit() const { return kind != PickKind::None; } }; @@ -31,7 +35,8 @@ struct PickResult namespace PickSystem { PickResult pick(const Camera& camera, - const DynArray<Text3dPtr>& words, - const DynArray<GameObjectPtr>& scenery, - const DynArray<Wall::Ptr>& wall_texts); + const DynArray<Text3dPtr>& words, + const DynArray<GameObjectPtr>& scenery, + const DynArray<Wall::Ptr>& wall_texts, + const DynArray<Text::Handler*>& walls); } diff --git a/src/systems/program_system.cpp b/src/systems/program_system.cpp index 09ad3dc..c26b0ff 100644 --- a/src/systems/program_system.cpp +++ b/src/systems/program_system.cpp @@ -1,5 +1,4 @@ #include <SDL3/SDL.h> -#include <algorithm> #include <cerrno> #include <fcntl.h> #include <signal.h> @@ -9,7 +8,6 @@ #include "entities/world.hpp" #include "systems/program_system.hpp" -#include "text_3d/text_3d.hpp" #include "utils/text.hpp" #include "utils/config.hpp" @@ -38,22 +36,16 @@ ProgramSystem::close_Child() void -ProgramSystem::run(Wall::PtrList& texts, float world_scale) +ProgramSystem::run(Text::Handler& out) { - // Clear previous output from the Program wall. - texts.erase( - std::remove_if(texts.begin(), texts.end(), - [](const Wall::Ptr& wt) { - return wt->role == Wall::Role::Program; - }), - texts.end()); + out.clear(); // wipe previous output from the Program wall _count = 0; - close_Child(); // stop any previous run cleanly + close_Child(); // stop any previous run cleanly // Nothing built yet — tell the player to compile first. if (access(kProgramPath, X_OK) != 0) { - push_Line(texts, "no program - compile first", world_scale); + push_Line(out, "no program - compile first"); return; } @@ -105,7 +97,7 @@ ProgramSystem::run(Wall::PtrList& texts, float world_scale) void -ProgramSystem::update(Wall::PtrList& texts, float world_scale) +ProgramSystem::update(Text::Handler& out) { if (_out_fd < 0) return; @@ -129,16 +121,16 @@ ProgramSystem::update(Wall::PtrList& texts, float world_scale) std::string line = _pending.substr(0, nl); _pending.erase(0, nl + 1); if (!line.empty() && line.back() == '\r') line.pop_back(); - push_Line(texts, line, world_scale); + push_Line(out, line); } } else if (n == 0) { // EOF — program finished. Flush any partial line and reap. if (!_pending.empty()) { - push_Line(texts, _pending, world_scale); + push_Line(out, _pending); _pending.clear(); } - if (_count == 0) push_Line(texts, "(no output)", world_scale); + if (_count == 0) push_Line(out, "(no output)"); close_Child(); return; } @@ -167,66 +159,37 @@ ProgramSystem::feed_Input(const std::string& text) void -ProgramSystem::echo_Input(Wall::PtrList& texts, const std::string& line) +ProgramSystem::echo_Input(Text::Handler& out, const std::string& line) { - push_Line(texts, line, World::get_BoxScale()); // scrolls up, lands at the prompt row + push_Line(out, line); // scrolls up, lands at the prompt row } void -ProgramSystem::echo_Words(Wall::PtrList& texts, DynArray<Text3dPtr>& words, float world_scale) +ProgramSystem::echo_Words(Text::Handler& out, DynArray<Text3dPtr>& words) { if (words.empty()) return; - // Make room: scroll existing program output up by one line. - for (Wall::Ptr& wt : texts) - if (wt->role == Wall::Role::Program) - wt->text3d->pos.y += static_cast<double>(line_height); - - Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, world_scale); - - // The words have already flown to the bottom line; re-home them as Program - // wall text (dark, like output) and snap their row onto start_y. - for (Text3dPtr& w : words) { - if (!w) continue; - w->set_Color(ConfigColor::dark_gray); - w->yaw = tf.yaw; - w->pos.y = start_y; // align the echoed row to the line - w->set_Look(ConfigColor::keep, Text3d::depth_wall); - - texts.push_back(cpputils::make_unique<Wall::Text>(Wall::Text{ - Wall::position_Of(Wall::Role::Program), - Wall::Role::Program, - std::move(w) - })); + // The flown word meshes are discarded; we echo their joined text as one + // output line (append handles the scroll + colour/placement). + std::string line; + for (const Text3dPtr& w : words) { + if (!w || w->type != ObjectType::Text) continue; + if (!line.empty()) line += ' '; + line += w->text; + } + words.clear(); + + if (!line.empty()) { + out.append_Line(line); + _count++; } - _count++; // counts as one printed line } void -ProgramSystem::push_Line(Wall::PtrList& texts, const std::string& line, float world_scale) +ProgramSystem::push_Line(Text::Handler& out, const std::string& line) { - std::string clean = Text::ascii_Sanitize(line); - - // Shift existing program-wall lines up so new output appears at the bottom. - for (Wall::Ptr& wt : texts) - if (wt->role == Wall::Role::Program) - wt->text3d->pos.y += static_cast<double>(line_height); - - Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, world_scale); - Vec3 pos = tf.position; - pos.y = start_y; - - // Program wall is light ivory → use a dark text colour for contrast. - Text3dPtr t = make_Text3d(clean, Text3d::depth_wall, 0.003f, - pos, ConfigColor::dark_gray); - t->yaw = tf.yaw; - - texts.push_back(cpputils::make_unique<Wall::Text>(Wall::Text{ - tf.wall_position, - Wall::Role::Program, - std::move(t) - })); + out.append_Line(Text::ascii_Sanitize(line)); _count++; } diff --git a/src/systems/program_system.hpp b/src/systems/program_system.hpp index 8c436d9..30a17ee 100644 --- a/src/systems/program_system.hpp +++ b/src/systems/program_system.hpp @@ -4,8 +4,8 @@ #include <sys/types.h> #include "entities/world.hpp" +#include "text/handler.hpp" #include "utils/globals.hpp" -#include "wall/wall.hpp" struct ProgramSystem @@ -19,18 +19,18 @@ private: std::string _pending; - void push_Line (Wall::PtrList&, const std::string& line, float world_scale); + void push_Line (Text::Handler& out, const std::string& line); void close_Child(); public: static inline float line_height = 0.4f; static inline float start_y = World::get_FloorY() + 1; - void run (Wall::PtrList&, float world_scale); - void update (Wall::PtrList&, float world_scale); + void run (Text::Handler& out); + void update (Text::Handler& out); void feed_Input (const std::string& text); - void echo_Input (Wall::PtrList&, const std::string& line); - void echo_Words (Wall::PtrList&, DynArray<Text3dPtr>& words, float world_scale); + void echo_Input (Text::Handler& out, const std::string& line); + void echo_Words (Text::Handler& out, DynArray<Text3dPtr>& words); bool is_Running () const { return _child > 0; } diff --git a/src/systems/pult_system.cpp b/src/systems/pult_system.cpp index 5b957cc..9185b5e 100644 --- a/src/systems/pult_system.cpp +++ b/src/systems/pult_system.cpp @@ -3,7 +3,7 @@ #include "pult_system.hpp" #include "rendering/gl_renderer.hpp" #include "systems/physics_system.hpp" -#include "text_3d/text_label.hpp" +#include "wall/label.hpp" #include "wall/wall.hpp" @@ -165,10 +165,10 @@ PultSystem::draw_Labels(GlRenderer& gl, const Camera& camera, const Light& light } -DynArray<TextLabel*> +DynArray<Text::Handler*> PultSystem::get_Labels() const { - DynArray<TextLabel*> out; + DynArray<Text::Handler*> out; out.reserve(_pults.size()); for (Pult* p : _pults) out.push_back(&p->label); return out; diff --git a/src/systems/pult_system.hpp b/src/systems/pult_system.hpp index 1d23239..17d299c 100644 --- a/src/systems/pult_system.hpp +++ b/src/systems/pult_system.hpp @@ -8,11 +8,12 @@ #include "utils/globals.hpp" class GlRenderer; -class TextLabel; struct Light; struct Player; struct ProjectileContactEvent; +namespace Text { class Handler; } + class PultSystem { @@ -40,7 +41,7 @@ public: Vec3 get_ConnectProgramPos () const; Vec3 get_ConnectEditorPos () const; - DynArray<TextLabel*> + DynArray<Text::Handler*> get_Labels () const; diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp index 04f7599..f53da0c 100644 --- a/src/systems/wall_connection.cpp +++ b/src/systems/wall_connection.cpp @@ -1,43 +1,18 @@ #include <SDL3/SDL.h> -#include <sstream> -#include "cpp_token/cpp_highlight.hpp" -#include "cpp_token/token_color.hpp" +#include "systems/wall_connection.hpp" +#include "text/handler.hpp" +#include "utils/math.hpp" + #include "entities/camera.hpp" #include "entities/world.hpp" #include "rendering/gl_renderer.hpp" #include "systems/program_system.hpp" -#include "systems/wall_connection.hpp" #include "utils/config.hpp" #include "utils/light.hpp" #include "wall/wall.hpp" -static double -mono_CharWidth() -{ - static double w = 0.0; - if (w == 0.0) { - Text3dPtr m1 = make_Text3d("M", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white); - Text3dPtr m2 = make_Text3d("MM", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white); - const double w1 = 2.0 * static_cast<double>(m1->half_extent); - const double w2 = 2.0 * static_cast<double>(m2->half_extent); - w = w2 - w1; - } - return w; -} -static double -mono_HalfExtentY() -{ - static double h = 0.0; - if (h == 0.0) { - Text3dPtr m = make_Text3d("M", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white); - h = static_cast<double>(m->half_extent_y); - } - return h; -} - - namespace Wall { @@ -47,36 +22,28 @@ Connection::begin_Program(const Vec3& pult_pos) { _target = Target::Program; _pult_pos = pult_pos; + _editor = nullptr; _prog_input.reset(); _prog_last.clear(); } void -Connection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y) +Connection::begin_Editor(const Vec3& pult_pos, const ::Text::Handler& editor) { _target = Target::Editor; _pult_pos = pult_pos; - load_Editor(lines, eye_y); -} - -void -Connection::load_Editor(const DynArray<std::string>& lines, double eye_y) -{ - std::vector<std::string> v(lines.begin(), lines.end()); - _vim.load(v); - if (_vim.is_Empty()) _vim.start_Insert(); - _vim_seen_version = _vim.get_Version(); - _eye_y = eye_y; - _status.clear(); - rebuild_Editor(); + _editor = &editor; // Handler is edited in place; we only draw chrome + _cmd_last.clear(); + _cmdline.reset(); } void Connection::end() { _target = Target::None; + _editor = nullptr; _prog_input.reset(); _prog_last.clear(); - _cmdline. reset(); _status. clear(); + _cmdline. reset(); _cmd_last. clear(); _blink = 0.0; } @@ -108,228 +75,10 @@ Connection::rebuild_ProgramPreview(const std::string& buffer) } -Connection::Request -Connection::feed_Vim(DynArray<int>& keys, double eye_y) -{ - _eye_y = eye_y; - - for (int k : keys) { - if (k == -1) _vim.key_enter(); - else if (k == -2) _vim.key_escape(); - else if (k == -3) _vim.key_backspace(); - else if (k > 0) _vim.key_char(static_cast<char>(k)); - } - - Request req = Request::None; - switch (_vim.take_Signal()) { - case VimBufferOLD::Signal::Exit: return Request::Exit; - case VimBufferOLD::Signal::WriteQuit: return Request::WriteQuit; - case VimBufferOLD::Signal::QuitDiscard: return Request::QuitDiscard; - case VimBufferOLD::Signal::Write: req = Request::Write; break; - case VimBufferOLD::Signal::Help: - _status.clear(); - _status.push_back("-- VIM HELP --"); - _status.push_back("i a A o O insert Esc normal h j k l move"); - _status.push_back("0 $ line ends gg G :N jump line x dd delete"); - _status.push_back(":w write :q quit :wq write+quit :q! discard"); - break; - default: break; - } - - if (_vim.get_Version() != _vim_seen_version) { - _vim_seen_version = _vim.get_Version(); - rebuild_Editor(); - } - return req; -} - - -DynArray<std::string> -Connection::get_Lines() const -{ - DynArray<std::string> out; - for (const std::string& l : _vim.get_Lines()) out.push_back(l); - return out; -} - - void -Connection::rebuild_Editor() -{ - const float ws = World::get_BoxScale(); - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws); - const double lh = ConfigWall::line_height; - const int R = _vim.get_Row(); - const int N = (int)_vim.get_Lines().size(); - const double bottom_y = -static_cast<double>(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; - _row0_y = SDL_min(anchor_cursor, anchor_bottom); - - const auto& lines = _vim.get_Lines(); - - const double char_w = mono_CharWidth(); - const double he_y = mono_HalfExtentY(); - - // Can we reuse cached meshes? Only when the line COUNT is unchanged — then - // a line whose text is identical keeps its existing token meshes (we only - // reposition). Lines whose text changed are rebuilt. Count change (enter, - // dd) → full rebuild (rare vs. per-char typing). - const bool can_reuse = (_cached_line_text.size() == lines.size()); - - // Move old state aside so we can salvage rows from it. - DynArray<EditorRow> old_rows = std::move(_rows); - DynArray<Text3dPtr> old_nums = std::move(_linenums); - _rows.clear(); - _linenums.clear(); - - // ── gutter width: build/reuse line-number meshes ── - // Numbers only change when the line count changes, so on reuse we keep them. - DynArray<Text3dPtr> nums; - nums.reserve(lines.size()); - double gutter_w = 0.0; - if (can_reuse && old_nums.size() == lines.size()) { - for (size_t i = 0; i < lines.size(); ++i) { - gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(old_nums[i]->half_extent)); - nums.push_back(std::move(old_nums[i])); - } - } else { - for (size_t i = 0; i < lines.size(); ++i) { - Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f, - tf.position, ConfigColor::orange); - num->yaw = tf.yaw; - num->set_Color(ConfigColor::orange); - gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent)); - nums.push_back(std::move(num)); - } - } - const double gutter_gap = 0.6; - const double code_u0 = gutter_w + gutter_gap; - - // ── Per-line token meshes + canonical source for lexing. ── - struct BuiltTok { Text3dPtr t; size_t col; size_t len; size_t byte_begin; size_t byte_end; }; - struct BuiltRow { DynArray<BuiltTok> toks; }; - DynArray<BuiltRow> built; - built.reserve(lines.size()); - - std::string source; // canonical buffer for tree-sitter (token-joined) - - double block_code_w = 0.0; - for (size_t i = 0; i < lines.size(); ++i) { - const std::string& s = lines[i]; - block_code_w = SDL_max(block_code_w, char_w * static_cast<double>(s.size())); - - BuiltRow br; - const bool reuse_line = can_reuse - && i < _cached_line_text.size() - && _cached_line_text[i] == s - && i < old_rows.size(); - - // Walk tokens once; build meshes (reuse or fresh) AND record byte ranges - // in the canonical source (space-joined tokens, newline-joined lines). - size_t pos = 0, ti = 0; - bool first_tok = true; - while (pos < s.size()) { - while (pos < s.size() && s[pos] == ' ') ++pos; - if (pos >= s.size()) break; - size_t start = pos; std::string tk; - while (pos < s.size() && s[pos] != ' ') { tk += s[pos]; ++pos; } - - if (!first_tok) source += ' '; - first_tok = false; - const size_t b0 = source.size(); - source += tk; - const size_t b1 = source.size(); - - Text3dPtr t; - if (reuse_line && ti < old_rows[i].toks.size()) { - t = std::move(old_rows[i].toks[ti]); - } else { - t = make_Text3d(tk, Text3d::depth_wall, 0.003f, tf.position, ConfigColor::white); - t->yaw = tf.yaw; - // NO TokenColor::apply — coloring comes solely from tree-sitter below. - } - br.toks.push_back({ std::move(t), start, tk.size(), b0, b1 }); - ++ti; - } - source += '\n'; - - built.push_back(std::move(br)); - } - - const double block_w = code_u0 + block_code_w; - const double z_block_left = block_w * 0.5; - _code_left_z = z_block_left - code_u0; - - // ── Place numbers + tokens (always reposition — cheap). ── - for (size_t i = 0; i < lines.size(); ++i) { - const double row_y = _row0_y - static_cast<double>(i) * lh; - - if (nums[i]) { - const double he = static_cast<double>(nums[i]->half_extent); - nums[i]->pos = tf.position; - nums[i]->pos.y = row_y; - nums[i]->pos.z = z_block_left - he; - _linenums.push_back(std::move(nums[i])); - } - - EditorRow er; - er.left_z = _code_left_z; - er.char_w = char_w; - er.he_y = he_y; - er.line_x = tf.position.x; - er.has_text = !built[i].toks.empty(); - - for (BuiltTok& bt : built[i].toks) { - const double left_z = _code_left_z - static_cast<double>(bt.col) * char_w; - const double tok_w = char_w * static_cast<double>(bt.len); - bt.t->pos = tf.position; - bt.t->pos.y = row_y; - bt.t->pos.z = left_z - tok_w * 0.5; - er.toks.push_back(std::move(bt.t)); - } - - _rows.push_back(std::move(er)); - } - - // Snapshot line text for next reuse check. - _cached_line_text.clear(); - for (const std::string& s : lines) _cached_line_text.push_back(s); - - // ── Color every token via tree-sitter (same classifier as the wall). ── - { - DynArray<HiSpan> spans = CppHighlight::classify(source); - // For each span, color the tokens whose byte range overlaps it. - // built[] holds byte ranges; _rows[] holds the meshes in the same order. - for (const HiSpan& span : spans) { - for (size_t i = 0; i < built.size() && i < _rows.size(); ++i) { - for (size_t j = 0; j < built[i].toks.size() && j < _rows[i].toks.size(); ++j) { - const BuiltTok& bt = built[i].toks[j]; - // overlap test: [bt.byte_begin,bt.byte_end) ∩ [span.begin,span.end) - if (bt.byte_begin < span.byte_end && span.byte_begin < bt.byte_end) { - if (_rows[i].toks[j]) - _rows[i].toks[j]->set_Color(TokenColor::get(span.type)); - } - } - } - } - } - - rebuild_Cmdline(); -} - - -void -Connection::rebuild_Cmdline() +Connection::rebuild_Cmdline(const std::string& text) { _cmdline.reset(); - - std::string text; - if (_vim.get_Mode() == VimBufferOLD::Mode::Command) text = _vim.get_Cmdline(); - else if (!_status.empty()) text = _status[0]; - else if (!_vim.get_Status().empty()) text = _vim.get_Status(); - else text = (_vim.get_Mode() == VimBufferOLD::Mode::Insert) ? "-- INSERT --" : "-- NORMAL --"; if (text.empty()) return; // Built at origin; render() places it at the eased _cmd_pos each frame. @@ -343,15 +92,20 @@ Connection::update(double dt, const Camera& cam, float aspect, float fov_y) if (!is_Active()) return; _blink += dt; + // Editor mode-line text comes from the Handler's vim state — rebuild the + // floating mesh only when it changes. + if (_target == Target::Editor && _editor) { + std::string ml = _editor->modeline(); + if (ml != _cmd_last) { rebuild_Cmdline(ml); _cmd_last = ml; } + } + 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); - // Sit at a fraction of the way toward the lower-left corner. 0 = center, - // 1 = exactly the frustum edge (text would be clipped at the very corner), - // so keep a margin. + // Sit a fraction of the way toward the lower-left frustum corner. const double fx = 0.82; // horizontal: 82% toward the left edge const double fy = 0.78; // vertical: 78% toward the bottom edge const double left = half_w * fx; @@ -384,11 +138,8 @@ Connection::render(GlRenderer& gl, const Camera& cam, const Light& light) const draw_Electric (gl, cam); } else if (_target == Target::Editor) { - for (const EditorRow& er : _rows) - for (const Text3dPtr& t : er.toks) - if (t) t->draw(gl, cam, light); - for (const Text3dPtr& n : _linenums) if (n) n->draw(gl, cam, light); - + // Tokens + line numbers are drawn by the Handler itself; we only add the + // editing chrome on top. if (_cmdline) { const double hw = static_cast<double>(_cmdline->half_extent); _cmdline->pos = Vec3{ @@ -436,21 +187,20 @@ Connection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& l void Connection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& light) const { + if (!_editor) return; if (SDL_fmod(_blink, 0.8) > 0.4) return; - const int row = _vim.get_Row(); - if (row < 0 || row >= (int)_rows.size()) return; + const ::Text::Handler::MonoMetrics m = _editor->mono_Metrics(); + if (m.char_w <= 0.0) return; // no mono layout yet - Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, World::get_BoxScale()); - const double lh = ConfigWall::line_height; - const double row_y = _row0_y - static_cast<double>(row) * lh; + const int row = _editor->cursor_Row(); + const int col = _editor->cursor_Col(); + const double row_y = _editor->row_Y(row); + const double cz = m.left_z - (static_cast<double>(col) + 0.5) * m.char_w; - const EditorRow& er = _rows[(size_t)row]; - const double cw = er.char_w; - const int col = _vim.get_Col(); - const double cz = er.left_z - (static_cast<double>(col) + 0.5) * cw; + Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, World::get_BoxScale()); - Vec3 c { er.line_x, row_y - (er.he_y + 0.06), cz }; + Vec3 c { tf.position.x, row_y - (m.he_y + 0.06), cz }; Mat4 view = buildViewMat4 (cam); Mat4 get_Model = buildModelMat4(c, tf.yaw); @@ -462,7 +212,7 @@ Connection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& li gl.set_Light (light); gl.set_Color (0.6f, 1.0f, 1.0f); gl.set_Emissive (0.6f, 1.0f, 1.0f, 0.9f); - gl.draw_Box (static_cast<float>(cw * 0.45), 0.015f, 0.03f); + gl.draw_Box (static_cast<float>(m.char_w * 0.45), 0.015f, 0.03f); gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f); } diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp index 474fea4..44fafbe 100644 --- a/src/systems/wall_connection.hpp +++ b/src/systems/wall_connection.hpp @@ -1,72 +1,50 @@ #pragma once #include <string> -#include <vector> #include "text_3d/text_3d.hpp" -#include "text_3d/vim_buffer.hpp" -#include "utils/globals.hpp" class Camera; class GlRenderer; struct Light; +namespace Text { class Handler; } -struct EditorRow -{ - DynArray<Text3dPtr> - toks; // per-token meshes, left→right (decreasing z) - double left_z { 0.0 }; // z of the row's left edge (start of char 0) - double char_w { 0.18 }; // mean character width in z (for cursor placement) - double he_y { 0.2 }; // glyph half-height (for cursor vertical offset) - double line_x { 0.0 }; // wall-plane x - bool has_text { false }; -}; - namespace Wall { +// Pure overlay: draws the editing chrome (cmdline / cursor / electric arc) on +// top of whatever owns the actual text. For the editor it reads cursor + layout +// state from a Text::Handler that is being vim-edited in place (Mono layout); +// the Handler draws its own tokens. For the program it owns a small stdin echo. class Connection { public: - enum class Target : uint8_t { None, Program, Editor }; - enum class Request : uint8_t { None, Exit, Write, WriteQuit, QuitDiscard }; + enum class Target : uint8_t { None, Program, Editor }; void update(double dt, const Camera& cam, float aspect, float fov_y); void render(GlRenderer& gl, const Camera& cam, const Light& light) const; void end (); bool is_Active () const { return _target != Target::None; } - Target get_Target() const { return _target; } - DynArray<std::string> - get_Lines () const; void set_ProgramBuffer (const std::string& buffer); - - void begin_Program(const Vec3& pult_pos); - void begin_Editor (const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y); - void load_Editor (const DynArray<std::string>& lines, double eye_y); - - void mark_Saved () { _vim.mark_Saved(); } void clear_ProgramPreview(); - Request feed_Vim (DynArray<int>& keys, double eye_y); + void begin_Program(const Vec3& pult_pos); + void begin_Editor (const Vec3& pult_pos, const ::Text::Handler& editor); private: - bool _cmd_pos_init { false }; - unsigned _vim_seen_version { 0 }; + bool _cmd_pos_init { false }; + float _cmd_yaw { 0.0f }; + double _blink { 0.0 }; - float _cmd_pitch { 0.0f }; - float _cmd_yaw { 0.0f }; - double _row0_y { 0.0 }; - double _eye_y { 0.0 }; - double _blink { 0.0 }; - double _code_left_z { 0.0 }; + Target _target { Target::None }; - Target _target{ Target::None }; + const ::Text::Handler* _editor { nullptr }; // editor model (Editor target only) Text3dPtr _cmdline; Text3dPtr _prog_input; @@ -74,17 +52,10 @@ private: Vec3 _cmd_pos {}; Vec3 _pult_pos{}; + std::string _cmd_last; std::string _prog_last; - VimBufferOLD _vim; - - DynArray<std::string> _cached_line_text; - DynArray<EditorRow> _rows; - DynArray<Text3dPtr> _linenums; - DynArray<std::string> _status; - - void rebuild_Editor (); - void rebuild_Cmdline(); + void rebuild_Cmdline (const std::string& text); void rebuild_ProgramPreview(const std::string& buffer); void draw_EditorCursor (GlRenderer& gl, const Camera& cam, const Light& light) const; void draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const; diff --git a/src/text/handler.cpp b/src/text/handler.cpp index f04120d..33481b1 100644 --- a/src/text/handler.cpp +++ b/src/text/handler.cpp @@ -1,15 +1,3 @@ -// text/handler.cpp -// -// The unified text document. Consolidates four older pieces: -// GameState::editor_PlaceLines -> relayout_Snapped -// Wall::Editor (placement/nums) -> resolve_Target / apply_Target / attach_Snapped / maintain_Numbers -// Text::View (lex mapping) -> render_Source / apply_Highlight -// Connection::rebuild_Editor -> relayout_Mono (+ shoot-merge attach_Mono) -// -// The Handler OWNS its tokens. Snapped is the shoot-and-snap gameplay layout; -// Mono is the monospace grid vim edits on. Lexing is async: the Handler produces -// a source string + generation and accepts spans back; GameState owns the thread. - #include <SDL3/SDL.h> #include <algorithm> #include <sstream> @@ -79,6 +67,7 @@ Handler::Handler(Wall::Role role, float world_scale, LayoutMode mode) _tf = Wall::get_Transform(role, world_scale); _line_height = ConfigWall::line_height; _word_spacing = ConfigWall::free_space_dist; + _append_color = ConfigColor::dark_gray; } void @@ -113,6 +102,8 @@ Handler::clear() { _lines.clear(); _last_src.clear(); + _label_off.clear(); + _label_text.clear(); mark_Dirty(); } @@ -154,11 +145,11 @@ Handler::append_Line(const std::string& text) Line ln; ln.y = _append_y; - TokenPtr t = make_Token(text, _depth, _scale, _tf.position, ConfigColor::dark_gray); + TokenPtr t = make_Token(text, _depth, _scale, _tf.position, _append_color); t->yaw = _tf.yaw; t->pos = _tf.position; t->pos.y = _append_y; - t->set_Color(ConfigColor::dark_gray); // Static; program output isn't lexed + t->set_Color(_append_color); // Static; program output isn't lexed ln.tokens.push_back(std::move(t)); _lines.push_back(std::move(ln)); @@ -310,8 +301,16 @@ Handler::apply_Target(const Target& t) Line fresh; fresh.y = t.new_y; fresh.line_height = _line_height; - _lines.push_back(std::move(fresh)); - return _lines.back(); + + // Keep _lines ordered top->bottom (descending y). as_Lines / render_Source / + // numbering all read array order, so the array order IS the document order; + // a line shot in above existing text must land at the matching index, not + // at the end (that's what made the extracted source come out reordered). + size_t pos = 0; + while (pos < _lines.size() && _lines[pos].y > fresh.y) ++pos; + auto it = _lines.insert(_lines.begin() + static_cast<std::ptrdiff_t>(pos), + std::move(fresh)); + return *it; } void @@ -545,8 +544,9 @@ Handler::as_Lines() const void Handler::relayout() { - if (_layout == LayoutMode::Mono) relayout_Mono(); - else relayout_Snapped(); + if (_layout == LayoutMode::Mono) relayout_Mono(); + else if (_layout == LayoutMode::Billboard) relayout_Billboard(); + else relayout_Snapped(); } void @@ -812,6 +812,7 @@ Handler::apply_Tilt(const Camera& cam) void Handler::update(double dt, const Camera& cam) { + if (_layout == LayoutMode::Billboard) { billboard_Update(dt, cam); return; } if (_layout == LayoutMode::Snapped && _show_numbers) maintain_Numbers(dt, cam); apply_Tilt(cam); @@ -996,4 +997,227 @@ Handler::cmdline_Text() const const DynArray<std::string>& Handler::status_Lines() const { return _status; } +std::string +Handler::modeline() const +{ + if (!_vim) return {}; + if (_vim->get_Mode() == VimBuffer::Mode::Command) return _vim->get_Cmdline(); + if (!_status.empty()) return _status[0]; + if (!_vim->get_Status().empty()) return _vim->get_Status(); + return (_vim->get_Mode() == VimBuffer::Mode::Insert) ? "-- INSERT --" + : "-- NORMAL --"; +} + + +// ───────────────────────────── billboard label ───────────────────────────── +// Port of TextLabel: one centred line that faces the eye (or a fixed yaw) and +// jiggles on kicks. The token meshes are built once (relayout_Billboard); the +// per-frame anchor follow + spring runs in billboard_Update. + +void +Handler::set_Style(SDL_FColor color, float depth, float scale) +{ + _style_color = color; + _depth = depth; + _scale = scale; +} + +void +Handler::set_Centre(const Vec3& p) +{ + _layout = LayoutMode::Billboard; + _pos_mode = PositionMode::Absolute; + _centre = p; + _anchor_ptr = nullptr; +} + +void +Handler::attach_To(const Vec3* anchor, const Vec3& offset) +{ + _layout = LayoutMode::Billboard; + _pos_mode = PositionMode::Anchor; + _anchor_ptr = anchor; + _attach_offset = offset; +} + +void +Handler::detach() +{ + _pos_mode = PositionMode::Absolute; + _anchor_ptr = nullptr; +} + +void +Handler::set_Text(const std::string& text) +{ + if (text == _label_text && !_lines.empty()) return; + _label_text = text; + _layout = LayoutMode::Billboard; + relayout_Billboard(); +} + +void +Handler::set_Text(const std::string& text, SDL_FColor color, float depth, float scale) +{ + set_Style(color, depth, scale); + _label_text.clear(); + set_Text(text); +} + +void +Handler::set_Collidable(bool on) +{ + _collidable = on; + if (_lines.empty()) return; + for (TokenPtr& w : _lines[0].tokens) { + if (!w) continue; + w->solid = on; + w->carriable = false; + w->interactable = false; + w->body.mode = on ? PhysicsMode::Static : PhysicsMode::Hover; + } +} + +const DynArray<TokenPtr>& +Handler::get_Words() const +{ + static const DynArray<TokenPtr> empty; + return _lines.empty() ? empty : _lines[0].tokens; +} + +Vec3 +Handler::label_Centre() const +{ + if (_pos_mode == PositionMode::Anchor && _anchor_ptr) + return { _anchor_ptr->x + _attach_offset.x, + _anchor_ptr->y + _attach_offset.y, + _anchor_ptr->z + _attach_offset.z }; + return _centre; +} + +void +Handler::relayout_Billboard() +{ + _lines.clear(); + _label_off.clear(); + + DynArray<std::string> toks; + { + std::istringstream ss(_label_text); + std::string w; + while (ss >> w) toks.push_back(w); + } + if (toks.empty()) return; + + DynArray<TokenPtr> built; + built.reserve(toks.size()); + for (const std::string& tk : toks) + built.push_back(make_Token(tk, _depth, _scale, Vec3{0,0,0}, _style_color)); + + float total_w = 0.0f; + for (const TokenPtr& t : built) total_w += t->half_extent * 2.0f + ConfigText3d::word_gap; + total_w -= ConfigText3d::word_gap; + + Line line; + float x_cur = -total_w * 0.5f; + for (TokenPtr& t : built) { + const float he = t->half_extent; + _label_off.push_back(static_cast<double>(x_cur + he)); + + t->carriable = false; + t->interactable = false; + t->solid = _collidable; + t->body.mode = _collidable ? PhysicsMode::Static : PhysicsMode::Hover; + t->body.restitution = 0.55; + t->jiggle_off = Vec3{}; + t->jiggle_vel = Vec3{}; + + line.tokens.push_back(std::move(t)); + x_cur += he * 2.0f + ConfigText3d::word_gap; + } + + _lines.push_back(std::move(line)); +} + +void +Handler::billboard_Update(double dt, const Camera& cam) +{ + if (_lines.empty()) return; + DynArray<TokenPtr>& words = _lines[0].tokens; + + const Vec3 centre = label_Centre(); + Vec3 right { 1, 0, 0 }; + float yaw_g { 0.0f }; + float pitch_g { 0.0f }; + + if (_billboard) { + Vec3 fwd = cam.pos - centre; + double flen = Math::length(fwd); + if (flen < 0.0001) return; + fwd = fwd / flen; + + right = Math::cross(Vec3{0, 1, 0}, fwd); + double rlen = Math::length(right); + if (rlen < 0.0001) return; + right = right / rlen; + + yaw_g = SDL_atan2f(static_cast<float>(fwd.x), static_cast<float>(fwd.z)); + pitch_g = -SDL_asinf (static_cast<float>(fwd.y)); + } else { + yaw_g = _face_yaw; + pitch_g = 0.0f; + right = { SDL_cos(_face_yaw), 0.0, -SDL_sin(_face_yaw) }; + } + + auto clamp = [](double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); }; + + for (size_t i = 0; i < words.size() && i < _label_off.size(); ++i) { + Token* t = words[i].get(); + if (!t || !t->loaded) continue; + + const double off = _label_off[i]; + const Vec3 anchor { + centre.x + right.x * off, + centre.y + right.y * off, + centre.z + right.z * off, + }; + + Vec3& o = t->jiggle_off; + Vec3& v = t->jiggle_vel; + const Vec3 acc { + -ConfigText3d::jiggle_stiff * o.x - ConfigText3d::jiggle_damp * v.x, + -ConfigText3d::jiggle_stiff * o.y - ConfigText3d::jiggle_damp * v.y, + -ConfigText3d::jiggle_stiff * o.z - ConfigText3d::jiggle_damp * v.z, + }; + v.x += acc.x * dt; v.y += acc.y * dt; v.z += acc.z * dt; + o.x += v.x * dt; o.y += v.y * dt; o.z += v.z * dt; + o.x = clamp(o.x, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); + o.y = clamp(o.y, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); + o.z = clamp(o.z, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); + + t->pos = { anchor.x + o.x, anchor.y + o.y, anchor.z + o.z }; + t->yaw = yaw_g; + t->pitch = pitch_g; + } +} + +void +Handler::kick(size_t word_index, const Vec3& projectile_vel, const Vec3& normal) +{ + if (_lines.empty()) return; + DynArray<TokenPtr>& words = _lines[0].tokens; + if (word_index >= words.size() || !words[word_index]) return; + + double v_into = -(projectile_vel.x * normal.x + + projectile_vel.y * normal.y + + projectile_vel.z * normal.z); + if (v_into < 0.0) v_into = 0.0; + const double mag = v_into * ConfigText3d::kick_strength; + + Vec3& v = words[word_index]->jiggle_vel; + v.x += -normal.x * mag; + v.y += -normal.y * mag; + v.z += -normal.z * mag; +} + } // namespace Text diff --git a/src/text/handler.hpp b/src/text/handler.hpp index 8736ca2..d52fb21 100644 --- a/src/text/handler.hpp +++ b/src/text/handler.hpp @@ -27,9 +27,9 @@ #include "entities/camera.hpp" #include "text/line.hpp" #include "text/token.hpp" +#include "text/vim_buffer.hpp" // VimBuffer (owned in Mono mode) #include "utils/globals.hpp" #include "utils/light.hpp" -#include "vim_buffer.hpp" // VimBuffer (owned in Mono mode) #include "wall/wall.hpp" // Wall::Role, Wall::Transform, Wall::Position class GlRenderer; @@ -37,7 +37,7 @@ class GlRenderer; namespace Text { -enum class LayoutMode : uint8_t { Snapped, Mono }; +enum class LayoutMode : uint8_t { Snapped, Mono, Billboard }; class Handler { @@ -79,6 +79,7 @@ public: void set_Lines (const DynArray<std::string>& lines); // (re)build from text; relayouts + marks dirty void append_Line(const std::string& text); // program output: scroll up, add at bottom void set_AppendBaseline(double y) { _append_y = y; } // bottom row y for append_Line (program wall) + void set_AppendColor (SDL_FColor c) { _append_color = c; } // output text color for append_Line // ── ownership transfer (the heart of the rewrite) ───────────────────── // detach: remove a token from its line, hand ownership out (pick / shoot-off). @@ -147,6 +148,26 @@ public: VimBuffer::Mode cursor_Mode () const; const std::string& cmdline_Text () const; const DynArray<std::string>& status_Lines () const; + std::string modeline () const; // mode/cmdline/status text for the overlay + + // ── billboard label (LayoutMode::Billboard) ─────────────────────────── + // One centred line of words that faces the eye (or a fixed yaw) and jiggles + // on projectile kicks. Mirrors the old TextLabel API verbatim so call sites + // swap a TextLabel for a Handler with no other change. detach() (no args) is + // the anchor-detach; detach(Token*) above is the ownership-transfer one. + 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); + void attach_To (const Vec3* anchor, const Vec3& offset = {}); + void detach (); + void set_Billboard (bool on) { _billboard = on; } + void set_FaceYaw (float y) { _face_yaw = y; } + void set_Style (SDL_FColor color, float depth, float scale); + void set_Collidable(bool on); + void kick (size_t word_index, const Vec3& projectile_vel, const Vec3& normal); + bool is_Empty () const { return _lines.empty() || _lines[0].tokens.empty(); } + const DynArray<TokenPtr>& get_Words() const; + void draw (GlRenderer& gl, const Camera& cam, const Light& light) const { render(gl, cam, light); } private: DynArray<Line> _lines; // single source of truth @@ -162,6 +183,7 @@ private: double _line_height { 0.0 }; double _word_spacing { 0.0 }; double _append_y { 0.0 }; // program-wall bottom row + SDL_FColor _append_color { 0.45f, 0.45f, 0.45f, 1.0f }; // append_Line text color DynArray<std::string> _status; // multi-line status (vim :help etc.), for the overlay @@ -187,6 +209,23 @@ private: cpputils::unique_ptr<VimBuffer> _vim; unsigned _vim_seen_version { 0 }; + // ── billboard-label state (LayoutMode::Billboard) ───────────────────── + enum class PositionMode : uint8_t { Absolute, Anchor }; + PositionMode _pos_mode { PositionMode::Absolute }; + const Vec3* _anchor_ptr { nullptr }; + Vec3 _centre {}; + Vec3 _attach_offset {}; + SDL_FColor _style_color { 1.0f, 1.0f, 0.0f, 1.0f }; + std::string _label_text; + DynArray<double> _label_off; // per-word centre offset along the row + bool _billboard { true }; + bool _collidable { true }; + float _face_yaw { 0.0f }; + + void relayout_Billboard(); // (re)build the single centred line + void billboard_Update (double dt, const Camera& cam); + Vec3 label_Centre () const; + // layout void relayout (); // dispatch on _layout void relayout_Snapped(); diff --git a/src/text/text.cpp b/src/text/text.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/text/text.hpp b/src/text/text.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/text/token.hpp b/src/text/token.hpp index 816b9aa..d45401c 100644 --- a/src/text/token.hpp +++ b/src/text/token.hpp @@ -111,6 +111,11 @@ struct Token final : public GameObject size_t byte_begin { 0 }; size_t byte_end { 0 }; + // ── billboard-label jiggle (only used in LayoutMode::Billboard; was + // Text::Handler's parallel _jiggle_off/_jiggle_vel arrays) ── + Vec3 jiggle_off {}; + Vec3 jiggle_vel {}; + // ── AI state (by-value; idle on document tokens, animated only on free // words by WordAiSystem — matches the old Text3d::ai member exactly) ── diff --git a/src/text_3d/text_3d.cpp b/src/text_3d/text_3d.cpp deleted file mode 100644 index 084e16d..0000000 --- a/src/text_3d/text_3d.cpp +++ /dev/null @@ -1,351 +0,0 @@ -#include <SDL3/SDL.h> - -#include "cpp_token/token_color.hpp" -#include "entities/camera.hpp" -#include "entities/world.hpp" -#include "rendering/gl_renderer.hpp" -#include "systems/cpp_highlight_system.hpp" -#include "text_3d.hpp" -#include "utils/frame_profiler.hpp" -#include "utils/globals.hpp" - - -static SDL_FColor -aiGlintColor(Text3dAI::State s) -{ - switch (s) { - case Text3dAI::State::Attack: return {1.0f, 0.85f, 0.75f, 1.0f}; - case Text3dAI::State::Flee: return {1.0f, 1.0f, 0.80f, 1.0f}; - case Text3dAI::State::Stunned: return {0.85f, 0.85f, 0.95f, 1.0f}; - case Text3dAI::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). Same vertex layout as the text mesh. -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 }; -} - - -Text3d::Text3d(const std::string& txt, - float dpth, float scle, - Vec3 position, SDL_FColor colr) - : GameObject(position, ObjectType::Text, - CollisionShape{}, RigidBody{PhysicsMode::Hover}, - true, true, true), - depth(dpth), scale(scle), color(colr), text(txt) -{ - set_Color(TokenColor::get(CppHighlight::classify_Word(text))); - - - rebuild_Mesh(); - body.hover_phase = Random::unit() * SDL_PI_D * 2.0; - - - // AI personality - ai.aggressiveness = Random::unit(); - ai.wiggle_phase = Random::unit() * SDL_PI_D * 2.0; - ai.wiggle_freq = 3.0 + Random::unit() * 4.0; // 3–7 rad/s - ai.speed = 2.5 + Random::unit() * 2.5; -} - - -Text3d::~Text3d() -{ - // Renderer owns the GL context; only delete if buffers were created. - 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); -} - - -Vec3 -Text3d::words_centroid(const DynArray<Text3dPtr>& words) -{ - Vec3 c{}; - double n = 0.0; - for (const Text3dPtr& w : words) { - if (!w) continue; - c += w->pos; - n += 1.0; - } - if (n > 0.0) c = c / n; - return c; -} - - -Vec3 -Text3d::get_Distance(Vec3 position) const { - return pos - position; -} - - -void -Text3d::set_Color(SDL_FColor c) -{ - color = c; -} - - -void -Text3d::set_Depth(float new_depth) -{ - if (new_depth == depth) return; - depth = new_depth; - rebuild_Mesh(); -} - - -void -Text3d::set_Look(SDL_FColor c, float new_depth) -{ - if (c.r == -1.0f) - color = TokenColor::get(CppHighlight::classify_Word(text)); - else if (c.g == -1.0f) { } - else color = c; - - if (new_depth == depth) return; - depth = new_depth; - rebuild_Mesh(); -} - - -void -Text3d::rebuild_Mesh() -{ - loaded = mesh.build(font_path, text, 64, depth, scale); - if (!loaded) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Text3d: 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 - }; - - // Pack GPU-ready interleaved buffer once. draw() just uploads/binds this. - 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; // mesh changed → re-upload on next draw -} - - -void -Text3d::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 -Text3d::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 = buildModelMat4 (pos, yaw, pitch); - Mat4 mvp = Mat4::mul (proj, Mat4::mul(view, model)); - - gl.set_Wireframe(false); - gl.set_Light (light); - gl.set_MVP (model, mvp); - gl.set_NormalMat(model); - gl.set_FaceShade(color.r, color.g, color.b, back_dim, front_tint); - - if (glow_enabled && glow_strength > 0.0f) - gl.set_Emissive(color.r, color.g, color.b, glow_strength); - else - gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); - - // Draw the actual word FIRST, while MVP/normal/face-shade are still correct. - 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 != Text3dAI::State::Idle) { - 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); - } -} diff --git a/src/text_3d/text_3d.hpp b/src/text_3d/text_3d.hpp index 0ddb0ae..0890f82 100644 --- a/src/text_3d/text_3d.hpp +++ b/src/text_3d/text_3d.hpp @@ -1,116 +1,25 @@ #pragma once +// +// src/text_3d/text_3d.hpp — forwarding shim +// +// The old `Text3d` is now `Text::Token` (in text/token.hpp). The pointer-level +// aliases `Text3d` / `Text3dPtr` live in utils/globals.hpp (forward-decl-safe). +// This header pulls in the full Token definition and adds the aliases that need +// the complete type: `Text3dAI` and `make_Text3d`. +// +// The old src/text_3d/text_3d.cpp is superseded by src/text/token.cpp and MUST +// be removed from the build (otherwise duplicate Token:: definitions at link). -#include <SDL3/SDL.h> -#include <string> +#include <utility> -#include "cpp_token/token_db.hpp" -#include "cpputils/unique_ptr.hpp" -#include "entities/game_object.hpp" -#include "text_3d/text_mesh.hpp" -#include "utils/config.hpp" -#include "utils/globals.hpp" -#include "utils/light.hpp" - - -class Camera; -class GlRenderer; - - -struct Text3dAI -{ - enum class State : uint8_t - { - Idle, - Attack, - Flee, - Stunned, - }; - - State state = State::Idle; - double state_timer = 0.0; - double aggressiveness = 0.0; - double phase_timer = 0.0; - double wiggle_phase = 0.0; - double wiggle_freq = 4.0; - double speed = 2.0; - - bool in_pause = false; - double hover_height_saved = 0.0; - double orbit_angle = 0.0; - double orbit_dir = 1.0; - bool has_stolen = false; - - DynArray<Text3dPtr> stolen; - Vec3 trail_dir {1.0, 0.0, 0.0}; -}; - - -struct Text3d final : public GameObject -{ - static inline const std::string font_path = - "/usr/share/fonts/dejavu/DejaVuSansMono.ttf"; - - static inline constexpr float depth_wall = 0.06f; - static inline constexpr float depth_floor = 0.14f; - static inline constexpr float depth_compiler = 0.02f; - static Vec3 words_centroid(const DynArray<Text3dPtr>& words); - - mutable bool gpu_dirty { true }; - mutable int gpu_index_count { 0 }; - mutable unsigned int gpu_vbo { 0 }; - mutable unsigned int gpu_ebo { 0 }; - - mutable unsigned int gpu_vbo_sparkle { 0 }; - mutable unsigned int gpu_ebo_sparkle { 0 }; - mutable int gpu_sparkle_index_count { 0 }; - - static constexpr int SPARKLE_TRAIL = 14; - mutable int sparkle_trail_count { 0 }; - mutable Uint64 sparkle_last_ms { 0 }; - mutable Vec3 sparkle_trail[SPARKLE_TRAIL]; - - - bool glow_enabled { false }; - bool loaded { false }; - bool front_tint { true }; - - float glow_strength{ 0.2f }; - float half_extent_y{ 0.2f }; - float back_dim { 0.45f }; - float depth; - float scale; - float half_extent { 0.0f }; - float pitch { 0.0f }; - - Vec3 ribbon_local {}; - - SDL_FColor color; - std::string text; - TextMesh mesh; - Text3dAI ai; - - DynArray<int> packed_indices; - DynArray<float> packed_verts; - - Text3d(const std::string& txt, - float depth, float scale, - Vec3 position, - SDL_FColor colr = ConfigColor::blue); - ~Text3d() override; - - Vec3 get_Distance(Vec3 position) const; - void set_Color (SDL_FColor c); - void set_Depth (float depth); - void set_Look (SDL_FColor c, float depth); - void rebuild_Mesh(); - void update (const Vec3& camera_pos, const double view_mat[9], float carry_dist = 1.5f); - void draw (GlRenderer& gl, const Camera& camera, const Light& light) const override; -}; +#include "cpp_token/token_db.hpp" // parity with the old header's transitive include +#include "text/token.hpp" // brings the full Text::Token (and globals' Text3d/Text3dPtr) +using Text3dAI = Text::Token::Ai; template<typename... Args> -cpputils::unique_ptr<Text3d> +Text3dPtr make_Text3d(Args&&... args) { - return cpputils::make_unique<Text3d>(std::forward<Args>(args)...); + return Text::make_Token(std::forward<Args>(args)...); } diff --git a/src/text_3d/text_label.cpp b/src/text_3d/text_label.cpp deleted file mode 100644 index 4e2b2eb..0000000 --- a/src/text_3d/text_label.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include <algorithm> -#include <sstream> - -#include "rendering/gl_renderer.hpp" -#include "text_3d/text_label.hpp" - -static DynArray<std::string> -split_words(const std::string& text) -{ - DynArray<std::string> out; - std::istringstream ss(text); - std::string word; - while (ss >> word) - out.push_back(word); - return out; -} - - -void -TextLabel::set_Text(const std::string& text) -{ - if (text == _current_text) return; - _current_text = text; - rebuild(text); -} - - -void -TextLabel::set_Text(const std::string& text, - SDL_FColor color, float depth, float scale) -{ - set_Style(color, depth, scale); - _current_text.clear(); - set_Text(text); -} - - -void -TextLabel::clear() -{ - _words .clear(); - _offsets .clear(); - _anchor_pos .clear(); - _jiggle_off .clear(); - _jiggle_vel .clear(); - _current_text.clear(); -} - - -void -TextLabel::set_Centre(const Vec3& p) -{ - _mode = PositionMode::Absolute; - _centre = p; - _anchor_ptr = nullptr; -} - - -void -TextLabel::attach_To(const Vec3* anchor, const Vec3& offset) -{ - _mode = PositionMode::Anchor; - _anchor_ptr = anchor; - _attach_offset = offset; -} - - -void -TextLabel::detach() -{ - _mode = PositionMode::Absolute; - _anchor_ptr = nullptr; -} - -void -TextLabel::set_Style(SDL_FColor color, float depth, float scale) -{ - _style_color = color; - _style_depth = depth; - _style_scale = scale; -} - - -void -TextLabel::set_Collidable(bool on) -{ - _collidable = on; - for (Text3dPtr& w : _words) { - if (!w) continue; - w->solid = on; - w->carriable = false; - w->interactable = false; - w->body.mode = on ? PhysicsMode::Static : PhysicsMode::Hover; - } -} - - -void -TextLabel::rebuild(const std::string& text) -{ - _words.clear(); - _offsets.clear(); - _anchor_pos.clear(); - _jiggle_off.clear(); - _jiggle_vel.clear(); - - DynArray<std::string> tokens = split_words(text); - if (tokens.empty()) return; - - struct Tmp { Text3dPtr obj; }; - DynArray<Tmp> tmp; - tmp.reserve(tokens.size()); - for (const std::string& tok : tokens) { - tmp.push_back({ make_Text3d(tok, _style_depth, _style_scale, - Vec3{0,0,0}, _style_color) }); - } - - float total_w = 0.0f; - for (auto& t : tmp) total_w += t.obj->half_extent * 2.0f + ConfigText3d::word_gap; - total_w -= ConfigText3d::word_gap; - - float x_cur = -total_w * 0.5f; - for (auto& t : tmp) { - float he = t.obj->half_extent; - _offsets.push_back(static_cast<double>(x_cur + he)); - - t.obj->carriable = false; - t.obj->interactable = false; - t.obj->solid = _collidable; - t.obj->body.mode = _collidable ? PhysicsMode::Static - : PhysicsMode::Hover; - t.obj->body.restitution = 0.55; - - _words.push_back(std::move(t.obj)); - _anchor_pos.push_back(Vec3{}); - _jiggle_off.push_back(Vec3{}); - _jiggle_vel.push_back(Vec3{}); - - x_cur += he * 2.0f + ConfigText3d::word_gap; - } -} - - -Vec3 -TextLabel::get_CurrentCentre() const -{ - if (_mode == PositionMode::Anchor && _anchor_ptr) { - return { - _anchor_ptr->x + _attach_offset.x, - _anchor_ptr->y + _attach_offset.y, - _anchor_ptr->z + _attach_offset.z - }; - } - return _centre; -} - - -void -TextLabel::update(double dt, const Camera& camera) -{ - if (_words.empty()) return; - - Vec3 centre = get_CurrentCentre(); - Vec3 right {1, 0, 0}; - float yaw_for_glyphs = 0.0f; - float pitch_for_glyphs = 0.0f; - - if (_billboard) { - Vec3 forward = camera.pos - centre; - double flen = Math::length(forward); - if (flen < 0.0001) return; - - forward = forward / flen; - - right = Math::cross(Vec3{0, 1, 0}, forward); - double rlen = Math::length(right); - if (rlen < 0.0001) return; - right = right / rlen; - - yaw_for_glyphs = SDL_atan2f(static_cast<float>(forward.x), - static_cast<float>(forward.z)); - - pitch_for_glyphs = -SDL_asinf(static_cast<float>(forward.y)); - } else { - yaw_for_glyphs = _face_yaw; - pitch_for_glyphs = 0.0f; - right = { SDL_cos(_face_yaw), 0.0, -SDL_sin(_face_yaw) }; - } - - for (size_t i = 0; i < _words.size(); ++i) { - Text3d& t = *_words[i]; - if (!t.loaded) continue; - - double off = _offsets[i]; - - Vec3 anchor{ - centre.x + right.x * off, - centre.y + right.y * off, - centre.z + right.z * off - }; - _anchor_pos[i] = anchor; - - Vec3& off3 = _jiggle_off[i]; - Vec3& vel3 = _jiggle_vel[i]; - - Vec3 acc{ - -ConfigText3d::jiggle_stiff * off3.x - ConfigText3d::jiggle_damp * vel3.x, - -ConfigText3d::jiggle_stiff * off3.y - ConfigText3d::jiggle_damp * vel3.y, - -ConfigText3d::jiggle_stiff * off3.z - ConfigText3d::jiggle_damp * vel3.z - }; - vel3.x += acc.x * dt; - vel3.y += acc.y * dt; - vel3.z += acc.z * dt; - off3.x += vel3.x * dt; - off3.y += vel3.y * dt; - off3.z += vel3.z * dt; - - auto clamp = [](double v, double lo, double hi) { - return v < lo ? lo : (v > hi ? hi : v); - }; - off3.x = clamp(off3.x, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); - off3.y = clamp(off3.y, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); - off3.z = clamp(off3.z, -ConfigText3d::jiggle_max_off, ConfigText3d::jiggle_max_off); - - t.pos.x = anchor.x + off3.x; - t.pos.y = anchor.y + off3.y; - t.pos.z = anchor.z + off3.z; - t.yaw = yaw_for_glyphs; - t.pitch = pitch_for_glyphs; - } -} - - -void -TextLabel::draw(GlRenderer& gl, const Camera& camera, const Light& light) const -{ - for (const Text3dPtr& w : _words) { - if (!w || !w->loaded) continue; - w->draw(gl, camera, light); - } -} - - -void -TextLabel::kick(size_t word_index, const Vec3& projectile_vel, const Vec3& normal) -{ - if (word_index >= _words.size()) return; - - double v_into = -(projectile_vel.x * normal.x + - projectile_vel.y * normal.y + - projectile_vel.z * normal.z); - if (v_into < 0.0) v_into = 0.0; - - double mag = v_into * ConfigText3d::kick_strength; - - Vec3& vel3 = _jiggle_vel[word_index]; - vel3.x += -normal.x * mag; - vel3.y += -normal.y * mag; - vel3.z += -normal.z * mag; -} diff --git a/src/text_3d/text_label.hpp b/src/text_3d/text_label.hpp deleted file mode 100644 index 1a83f94..0000000 --- a/src/text_3d/text_label.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include <string> - -#include "entities/camera.hpp" -#include "text_3d/text_3d.hpp" -#include "utils/globals.hpp" -#include "utils/light.hpp" - - -class GlRenderer; - - -class TextLabel -{ - enum class PositionMode : uint8_t - { - Absolute, - Anchor, - }; - - bool _collidable { true }; - bool _billboard { true }; - - float _style_depth{ 0.06f }; - float _style_scale{ 0.003f }; - float _face_yaw { 0.0f }; - SDL_FColor _style_color{ 1.0f, 1.0f, 0.0f, 1.0f }; - - const Vec3* _anchor_ptr { nullptr }; - Vec3 _centre {}; - Vec3 _attach_offset{}; - - std::string _current_text; - - PositionMode _mode { PositionMode::Absolute }; - - DynArray<Text3dPtr> _words; - DynArray<double> _offsets; - DynArray<Vec3> _anchor_pos; - DynArray<Vec3> _jiggle_off; - DynArray<Vec3> _jiggle_vel; - - - void rebuild(const std::string& text); - Vec3 get_CurrentCentre() const; - -public: - TextLabel() = default; - - void clear (); - void update (double dt, const Camera& camera); - void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; - void kick (size_t word_index, const Vec3& projectile_vel, const Vec3& normal); - - void attach_To (const Vec3* anchor, const Vec3& offset = {}); - void detach (); - - void set_Centre (const Vec3& p); - void set_Billboard (bool on) { _billboard = on; } - void set_FaceYaw (float y) { _face_yaw = y; } - void set_Style (SDL_FColor color, float depth, float scale); - void set_Collidable(bool on); - void set_Text (const std::string& text); - void set_Text (const std::string& text, - SDL_FColor color, float depth, float scale); - - bool is_Empty () const { return _words.empty(); } - const DynArray<Text3dPtr>& - get_Words () const { return _words; } -}; diff --git a/src/text_3d/vim_buffer.cpp b/src/text_3d/vim_buffer.cpp deleted file mode 100644 index b67aa1c..0000000 --- a/src/text_3d/vim_buffer.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "vim_buffer.hpp" - - -void -VimBufferOLD::load(const std::vector<std::string>& src) -{ - _lines = src; - if (_lines.empty()) _lines.push_back(""); - _row = _col = 0; _mode = Mode::Normal; - _count = 0; - _sig = Signal::None; ++_version; - _saved = _lines; - _dirty = false; - - _pending.clear(); - _cmd .clear(); - _status .clear(); -} - - -void -VimBufferOLD::key_char(char c) -{ - if (_mode == Mode::Insert) { insert_Char(c); } - else if (_mode == Mode::Command) { _cmd.push_back(c); touch(); } - else { normal_Key(c); } -} - -void -VimBufferOLD::key_enter() -{ - if (_mode == Mode::Insert) { split_Line(); } - else if (_mode == Mode::Command) { run_Command(); } - else { _row = get_ClampRow(_row + 1); clamp_Col(); touch(); } -} - -void VimBufferOLD::key_backspace() -{ - if (_mode == Mode::Insert) { backspace_Insert(); } - else if (_mode == Mode::Command) { if (_cmd.size() > 1) _cmd.pop_back(); else cancel_Command(); touch(); } - else { _col = (_col > 0 ? _col - 1 : 0); touch(); } -} - -void VimBufferOLD::key_escape() -{ - if (_mode == Mode::Command) { cancel_Command(); return; } - _mode = Mode::Normal; - if (_col > 0) --_col; - clamp_Col(); touch(); -} - - -VimBufferOLD::Signal -VimBufferOLD::take_Signal() -{ Signal s = _sig; _sig = Signal::None; return s; } - - -void -VimBufferOLD::mark_Saved() -{ _saved = _lines; _dirty = false; touch(); } - - -void -VimBufferOLD::touch() -{ ++_version; } - -int -VimBufferOLD::get_LineLen() const -{ return (int)_lines[(size_t)_row].size(); } - -int -VimBufferOLD::get_ClampRow(int r) const -{ int n=(int)_lines.size(); return r<0?0:(r>=n?n-1:r); } - -void -VimBufferOLD::clamp_Col() -{ - int maxc = (_mode==Mode::Insert) ? get_LineLen() : (get_LineLen()>0 ? get_LineLen()-1 : 0); - if (_col < 0) _col = 0; - if (_col > maxc) _col = maxc; -} - -int -VimBufferOLD::get_Eff() const -{ return _count > 0 ? _count : 1; } - -void -VimBufferOLD::normal_Key(char c) -{ - if (c == ':') { _mode = Mode::Command; _cmd = ":"; - _status.clear(); touch(); return; } - - if (std::isdigit((unsigned char)c)) { - if (c == '0' && _count == 0 && _pending.empty()) - { _col = 0; touch(); return; } - _count = _count * 10 + (c - '0'); return; - } - - if (_pending == "d") { _pending.clear(); - if (c == 'd') { delete_Lines(get_Eff()); _count = 0; - touch(); return; } - _count = 0; return; - } - if (_pending == "g") { _pending.clear(); - if (c == 'g') { _row = get_ClampRow(_count > 0 ? _count - 1 : 0); - _count = 0; clamp_Col(); touch(); return; } - _count = 0; return; - } - - switch (c) { - case 'h': _col -= get_Eff(); break; - case 'l': _col += get_Eff(); break; - case 'j': _row = get_ClampRow(_row + get_Eff()); break; - case 'k': _row = get_ClampRow(_row - get_Eff()); break; - case '0': _col = 0; break; - case '$': _col = get_LineLen() > 0 ? get_LineLen() - 1 : 0; break; - case 'x': delete_Chars(get_Eff()); break; - case 'i': _mode = Mode::Insert; break; - case 'a': _col = (get_LineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break; - case 'A': _col = get_LineLen(); _mode = Mode::Insert; break; - case 'o': open_Below(); _mode = Mode::Insert; break; - case 'O': open_Above(); _mode = Mode::Insert; break; - case 'G': _row = get_ClampRow(_count > 0 ? _count - 1 : (int)_lines.size() - 1);break; - case 'd': _pending = "d"; return; - case 'g': _pending = "g"; return; - default: break; - } - _count = 0; clamp_Col(); touch(); -} - -void -VimBufferOLD::insert_Char(char c) -{ - std::string& L = _lines[(size_t)_row]; - if (_col > (int)L.size()) _col = (int)L.size(); - L.insert((size_t)_col, 1, c); ++_col; _dirty = true; touch(); -} - -void -VimBufferOLD::split_Line() -{ - std::string& L = _lines[(size_t)_row]; - std::string tail = L.substr((size_t)_col); - L.erase((size_t)_col); - _lines.insert(_lines.begin() + _row + 1, tail); - ++_row; _col = 0; _dirty = true; touch(); -} - -void -VimBufferOLD::backspace_Insert() -{ - if (_col > 0) { _lines[(size_t)_row].erase((size_t)(_col-1), 1); --_col; _dirty = true; } - else if (_row > 0) { - int prevlen = (int)_lines[(size_t)(_row-1)].size(); - _lines[(size_t)(_row-1)] += _lines[(size_t)_row]; - _lines.erase(_lines.begin() + _row); - --_row; _col = prevlen; _dirty = true; - } - touch(); -} - -void -VimBufferOLD::delete_Chars(int n) -{ - std::string& L = _lines[(size_t)_row]; - for (int i = 0; i < n && _col < (int)L.size(); ++i) { L.erase((size_t)_col, 1); _dirty = true; } - clamp_Col(); -} - -void -VimBufferOLD::delete_Lines(int n) -{ - for (int i = 0; i < n && (int)_lines.size() > 0; ++i) { - if (_lines.size() == 1) { _lines[0].clear(); _dirty = true; break; } - _lines.erase(_lines.begin() + _row); - if (_row >= (int)_lines.size()) _row = (int)_lines.size() - 1; - _dirty = true; - } - clamp_Col(); -} -void -VimBufferOLD::open_Below() -{ _lines.insert(_lines.begin() + _row + 1, ""); ++_row; _col = 0; _dirty = true; } - -void -VimBufferOLD::open_Above() -{ _lines.insert(_lines.begin() + _row, ""); _col = 0; _dirty = true; } - -void -VimBufferOLD::run_Command() -{ - std::string c = _cmd.size() ? _cmd.substr(1) : ""; - _cmd.clear(); _mode = Mode::Normal; - - if (c == "w" || c == "write") { - _sig = Signal::Write; - } - else if (c == "wq" || c == "x") { - _sig = Signal::WriteQuit; - } - else if (c == "q" || c == "quit") { - if (_dirty) { - _status = "No write since last change (add ! to override)"; - } else { - _sig = Signal::Exit; - } - } - else if (c == "q!" || c == "quit!") { - _lines = _saved; - _row = get_ClampRow(_row); clamp_Col(); - _dirty = false; - _sig = Signal::QuitDiscard; - } - else if (!c.empty() && std::all_of(c.begin(), c.end(), - [](char ch){ return std::isdigit((unsigned char)ch); })) { - _row = get_ClampRow(std::stoi(c) - 1); clamp_Col(); - } - else _status = "not a command: " + c; - touch(); -} - -void -VimBufferOLD::cancel_Command() -{ _cmd.clear(); _mode = Mode::Normal; touch(); } - -bool -VimBufferOLD::is_Dirty() const -{ return _dirty; } diff --git a/src/text_3d/vim_buffer.hpp b/src/text_3d/vim_buffer.hpp deleted file mode 100644 index d470e73..0000000 --- a/src/text_3d/vim_buffer.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include <algorithm> -#include <cstdint> -#include <string> -#include <vector> - -class VimBufferOLD { -public: - enum class Mode : uint8_t { Normal, Insert, Command }; - enum class Signal : uint8_t { - None, - Exit, - Help, - Write, - WriteQuit, - QuitDiscard - }; - - void key_char (char c); - void key_enter (); - void key_backspace(); - void key_escape (); - - void load (const std::vector<std::string>& src); - Signal take_Signal(); - void mark_Saved (); - - 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; } - unsigned - get_Version () const { return _version; } - const std::string& - get_Cmdline () const { return _cmd; } - const std::string& - get_Status () const { return _status; } - void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); } - -private: - bool _dirty = false; - unsigned _version = 0; - int _row = 0, _col = 0; - int _count = 0; - - std::string _pending; - std::string _cmd; - std::string _status; - - Mode _mode = Mode::Normal; - Signal _sig = Signal::None; - std::vector<std::string> _saved; - std::vector<std::string> _lines{ "" }; - - void touch (); - void clamp_Col (); - void normal_Key (char c); - void insert_Char (char c); - void split_Line (); - void backspace_Insert(); - void delete_Chars (int n); - void delete_Lines (int n); - void open_Below (); - void open_Above (); - void run_Command (); - void cancel_Command (); - - bool is_Dirty () const; - int get_LineLen () const; - int get_Eff () const; - int get_ClampRow (int r) const; -}; diff --git a/src/utils/globals.hpp b/src/utils/globals.hpp index ed1d089..d225217 100644 --- a/src/utils/globals.hpp +++ b/src/utils/globals.hpp @@ -12,14 +12,16 @@ class GlRenderer; struct CarriedWordNode; struct GameObject; -struct Text3d; +namespace Text { struct Token; } using Math::Vec3; -template<typename T> +template<typename T> using DynArray = std::vector<T>; using GlRendererPtr = cpputils::unique_ptr<GlRenderer>; -using Text3dPtr = cpputils::unique_ptr<Text3d>; using CarriedWordNodePtr = cpputils::unique_ptr<CarriedWordNode>; using GameObjectPtr = cpputils::unique_ptr<GameObject>; + +using Text3d = Text::Token; +using Text3dPtr = cpputils::unique_ptr<Text::Token>; diff --git a/src/utils/text.cpp b/src/utils/text.cpp index aba7de4..991604f 100644 --- a/src/utils/text.cpp +++ b/src/utils/text.cpp @@ -91,18 +91,18 @@ ascii_Sanitize(const std::string& in) void -View::clear() +SrcView::clear() { lines.clear(); } std::string -View::render_Source() +SrcView::render_Source() { std::string out; - for (Line& line : lines) { + for (SrcLine& line : lines) { for (size_t ti = 0; ti < line.tokens.size(); ++ti) { if (ti) out += ' '; - Token& tk = line.tokens[ti]; + SrcToken& tk = line.tokens[ti]; tk.byte_begin = out.size(); out += tk.text; tk.byte_end = out.size(); @@ -113,11 +113,11 @@ View::render_Source() } -Token* -View::token_At(size_t off) +SrcToken* +SrcView::token_At(size_t off) { - for (Line& line : lines) - for (Token& tk : line.tokens) + for (SrcLine& line : lines) + for (SrcToken& tk : line.tokens) if (off >= tk.byte_begin && off < tk.byte_end) return &tk; return nullptr; @@ -125,10 +125,10 @@ View::token_At(size_t off) void -View::apply_Spans(const DynArray<HiSpan>& spans) +SrcView::apply_Spans(const DynArray<HiSpan>& spans) { - for (Line& line : lines) { - for (Token& tk : line.tokens) { + for (SrcLine& line : lines) { + for (SrcToken& tk : line.tokens) { if (!tk.view) continue; size_t best_overlap = 0; @@ -148,10 +148,10 @@ View::apply_Spans(const DynArray<HiSpan>& spans) } DynArray<std::string> -View::as_Lines() const +SrcView::as_Lines() const { DynArray<std::string> out; - for (const Line& line : lines) { + for (const SrcLine& line : lines) { std::string s; for (size_t ti = 0; ti < line.tokens.size(); ++ti) { if (ti) s += ' '; diff --git a/src/utils/text.hpp b/src/utils/text.hpp index 57e0522..aaa1051 100644 --- a/src/utils/text.hpp +++ b/src/utils/text.hpp @@ -18,8 +18,13 @@ std::string ascii_Sanitize(const std::string& in); +// ── Source view (temporary) ──────────────────────────────────────────────── +// Renamed from Token/Line/View to Src* so it no longer collides with the new +// Text::Token. This whole trio is replaced by Text::Handler and will be deleted +// once the editor cutover lands; it survives one step purely to keep the build +// green during the type unification. -struct Token +struct SrcToken { std::string text; size_t byte_begin { 0 }; @@ -28,19 +33,18 @@ struct Token }; -struct Line +struct SrcLine { - DynArray<Token> tokens; + DynArray<SrcToken> tokens; }; -struct View +struct SrcView { - - DynArray<Line> lines; + DynArray<SrcLine> lines; void clear (); std::string render_Source(); - Token* token_At (size_t off); + SrcToken* token_At (size_t off); void apply_Spans (const DynArray<HiSpan>& spans); DynArray<std::string> as_Lines () const; diff --git a/src/wall/label.cpp b/src/wall/label.cpp index dde80d5..d2e4ee1 100644 --- a/src/wall/label.cpp +++ b/src/wall/label.cpp @@ -1,8 +1,10 @@ #include "entities/camera.hpp" #include "rendering/gl_renderer.hpp" #include "utils/config.hpp" +#include "wall/label.hpp" #include "wall/wall.hpp" + // ── Label appearance ────────────────────────────────────────────────────────── namespace { @@ -100,7 +102,7 @@ Wall::Label::on_Hit(size_t word_index, const Vec3& projectile_vel) void Wall::Label::update(double dt, const Camera& camera) { - // Positions + jiggle spring first. In non-billboard mode TextLabel::update + // Positions + jiggle spring first. In non-billboard mode Text::Handler::update // forces every word's pitch to 0, so we must apply our eye-tilt AFTER it, // not before — otherwise the tilt gets clobbered. Pitch doesn't affect the // positions update() computes, so ordering it last is safe. diff --git a/src/wall/label.hpp b/src/wall/label.hpp new file mode 100644 index 0000000..38d9415 --- /dev/null +++ b/src/wall/label.hpp @@ -0,0 +1,29 @@ +#pragma once +// +// Wall::Label — a per-wall billboard name tag. Lives in its own header (not +// wall.hpp) because it now owns a Text::Handler by value, and handler.hpp +// includes wall.hpp; keeping Label here avoids the include cycle. + +#include "text/handler.hpp" +#include "wall/wall.hpp" + +class Camera; +class GlRenderer; +struct Light; + +namespace Wall +{ + struct Label + { + Wall::Role role; + float face_yaw; + Vec3 anchor; + ::Text::Handler label; + + static DynArray<Label> generate_All(float world_scale); + + void on_Hit (size_t word_index, const Vec3& projectile_vel); + void update (double dt, const Camera& camera); + void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; + }; +} diff --git a/src/wall/text.cpp b/src/wall/text.cpp index 920c0f3..6bc906d 100644 --- a/src/wall/text.cpp +++ b/src/wall/text.cpp @@ -1,5 +1,6 @@ #include "entities/world.hpp" #include "systems/cpp_highlight_system.hpp" +#include "text/handler.hpp" #include "utils/config.hpp" #include "wall/wall.hpp" @@ -129,32 +130,20 @@ Text::draw(GlRenderer& g, const Camera& c, const Light& l) const void -Text::show_TokenInfo(PtrList& texts, const std::string& token_text) +Text::show_TokenInfo(::Text::Handler& info, const std::string& token_text) { - texts.erase( - std::remove_if(texts.begin(), texts.end(), - [](const Ptr& wt){ return wt->role == Wall::Role::Info; }), - texts.end()); - - Wall::Transform tf = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale()); - - TokenType tt = CppHighlight::classify_Word(token_text); - TokenDB::Desc d = TokenDB::get_Description(token_text, tt); - - std::string line1 = token_text + " [" + TokenDB::get_TypeName(tt) + "]"; - std::string line2 = d.short_desc; - std::string line3 = d.long_desc; - - float y = -14.0f; - for (const std::string& line : { line1, line2, line3 }) { - if (line.empty()) { y -= ConfigWall::text_line_spacing; continue; } - Vec3 pos = tf.position; pos.y = y; - Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.003f, pos, ConfigColor::white); - t->yaw = tf.yaw; - texts.push_back(cpputils::make_unique<Text>(Text{ - tf.wall_position, Wall::Role::Info, std::move(t) })); - y -= ConfigWall::text_line_spacing; - } + info.clear(); + + TokenType tt = CppHighlight::classify_Word(token_text); + TokenDB::Desc d = TokenDB::get_Description(token_text, tt); + + const std::string line1 = token_text + " [" + TokenDB::get_TypeName(tt) + "]"; + const std::string line2 = d.short_desc; + const std::string line3 = d.long_desc; + + // Bottom-aligned: each append pushes the prior lines up, so line1 ends on top. + for (const std::string& line : { line1, line2, line3 }) + if (!line.empty()) info.append_Line(line); } @@ -177,52 +166,4 @@ Text::tilt_TowardEye(const Vec3& text_pos, const Vec3& eye_pos) } -void -Text::clear_ToFloor(Wall::PtrList& texts, - DynArray<Text3dPtr>& words, - Wall::Role role) -{ - for (Wall::Ptr& wt : texts) { - if (wt->role != role || !wt->text3d) continue; - - Text3dPtr word = std::move(wt->text3d); - word->body.mode = PhysicsMode::Dynamic; - word->body.vel = { 0.0, 1.5, 0.0 }; - word->body.hover_height = World::get_FloorY<double>() + 1.5; - - word->set_Look(ConfigColor::floor, Text3d::depth_floor); - - words.push_back(std::move(word)); - } - - texts.erase( - std::remove_if(texts.begin(), texts.end(), - [role](const Wall::Ptr& wt){ - return wt->role == role && !wt->text3d; - }), - texts.end()); -} - - -void -Text::scroll(Wall::PtrList& texts, Wall::Role role, float delta) -{ - for (Wall::Ptr& wt : texts) { - if (wt->role == role && wt->text3d) { - wt->text3d->pos.y += static_cast<double>(delta); - } - } -} - - -void -Text::scroll_Up (Wall::PtrList& texts, Wall::Role role) -{ scroll(texts, role, ConfigWall::scroll_step); } - - -void -Text::scroll_Down(Wall::PtrList& texts, Wall::Role role) -{ scroll(texts, role, -ConfigWall::scroll_step); } - - } // namespace Wall diff --git a/src/wall/wall.hpp b/src/wall/wall.hpp index 08c349a..3f4ca42 100644 --- a/src/wall/wall.hpp +++ b/src/wall/wall.hpp @@ -1,11 +1,12 @@ #pragma once #include "text_3d/text_3d.hpp" -#include "text_3d/text_label.hpp" #include "utils/config.hpp" #include "utils/globals.hpp" +namespace Text { class Handler; } + namespace Wall { struct Text; @@ -56,32 +57,10 @@ namespace Wall void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; - static void show_TokenInfo(PtrList& texts, const std::string& token_text); + static void show_TokenInfo(::Text::Handler& info, const std::string& token_text); static Ptr from_Drop (Text3dPtr src); static std::string extract_Text (const PtrList&, Wall::Role, double line_threshold = 0.3); static float tilt_TowardEye(const Vec3& text_pos, const Vec3& eye_pos); - static void clear_ToFloor (Wall::PtrList& texts, - DynArray<Text3dPtr>& words, - Wall::Role role); - static void scroll (Wall::PtrList& texts, Wall::Role role, float delta); - static void scroll_Up (Wall::PtrList& texts, Wall::Role role); - static void scroll_Down (Wall::PtrList& texts, Wall::Role role); }; - - - struct Label - { - Wall::Role role; - float face_yaw; - Vec3 anchor; - TextLabel label; - - static DynArray<Label> - generate_All(float world_scale); - - void on_Hit (size_t word_index, const Vec3& projectile_vel); - void update (double dt, const Camera& camera); - void draw (GlRenderer& gl, const Camera& camera, const Light& light) const; - }; }