From 68a3a3f3e944756a276b0f0a8005e9dedaf0e2ef Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 26 May 2024 23:34:49 +0100 Subject: [PATCH] Add Statics Window and interaction with statics in 3D view (#1240) Add Statics Window. Can be created using Ctrl + S or via the menu. Add more interaction with statics into the viewer - context menu hiding, clicking. Closes #912 --- README.md | 1 + doc/lua/staticmesh.md | 2 + trview.app.tests/ApplicationTests.cpp | 23 +- trview.app.tests/Elements/LevelTests.cpp | 26 +- trview.app.tests/Elements/RoomTests.cpp | 36 ++- trview.app.tests/Elements/StaticMeshTests.cpp | 9 + .../Lua/Elements/Lua_StaticMeshTests.cpp | 40 +++ .../PluginsWindowManagerTests.cpp | 2 +- .../Settings/SettingsLoaderTests.cpp | 25 ++ trview.app.tests/UI/ViewerUITests.cpp | 28 ++ .../Windows/StaticsWindowManagerTests.cpp | 89 ++++++ trview.app.tests/trview.app.tests.vcxproj | 1 + .../trview.app.tests.vcxproj.filters | 3 + trview.app.ui.tests/SettingsWindowTests.cpp | 39 +++ trview.app.ui.tests/StaticsWindowTests.cpp | 177 +++++++++++ trview.app.ui.tests/StaticsWindowTests.h | 5 + trview.app.ui.tests/trview.app.ui.tests.cpp | 6 + .../trview.app.ui.tests.vcxproj | 2 + .../trview.app.ui.tests.vcxproj.filters | 6 + trview.app.ui.tests/trview_tests.cpp | 2 + trview.app/Application.cpp | 22 +- trview.app/Application.h | 6 +- trview.app/ApplicationCreate.cpp | 6 +- trview.app/Elements/ILevel.h | 1 + trview.app/Elements/IStaticMesh.h | 9 + trview.app/Elements/Level.cpp | 36 ++- trview.app/Elements/Level.h | 4 + trview.app/Elements/Room.cpp | 19 +- trview.app/Elements/StaticMesh.cpp | 79 +++-- trview.app/Elements/StaticMesh.h | 11 +- trview.app/Geometry/IMesh.cpp | 6 +- trview.app/Geometry/PickResult.cpp | 8 + .../Elements/StaticMesh/Lua_StaticMesh.cpp | 19 +- trview.app/Mocks/Elements/ILevel.h | 1 + trview.app/Mocks/Elements/IStaticMesh.h | 26 +- trview.app/Mocks/Mocks.cpp | 8 + trview.app/Mocks/UI/ISettingsWindow.h | 1 + trview.app/Mocks/Windows/IStaticsWindow.h | 22 ++ .../Mocks/Windows/IStaticsWindowManager.h | 21 ++ trview.app/Resources/resource.h | 1 + trview.app/Resources/trview.app.rc | 1 + trview.app/Settings/SettingsLoader.cpp | 2 + trview.app/Settings/UserSettings.cpp | 1 + trview.app/Settings/UserSettings.h | 1 + trview.app/UI/ISettingsWindow.h | 2 + trview.app/UI/SettingsWindow.cpp | 6 + trview.app/UI/SettingsWindow.h | 3 + trview.app/UI/ViewerUI.cpp | 2 + trview.app/Windows/ColumnSizer.cpp | 5 + trview.app/Windows/IViewer.h | 2 + trview.app/Windows/RoomsWindow.cpp | 3 +- trview.app/Windows/Statics/IStaticsWindow.h | 27 ++ .../Windows/Statics/IStaticsWindowManager.h | 22 ++ trview.app/Windows/Statics/StaticsWindow.cpp | 288 ++++++++++++++++++ trview.app/Windows/Statics/StaticsWindow.h | 60 ++++ .../Windows/Statics/StaticsWindowManager.cpp | 68 +++++ .../Windows/Statics/StaticsWindowManager.h | 34 +++ trview.app/Windows/Viewer.cpp | 17 +- trview.app/trview.app.vcxproj | 8 + trview.app/trview.app.vcxproj.filters | 27 ++ 60 files changed, 1356 insertions(+), 51 deletions(-) create mode 100644 trview.app.tests/Windows/StaticsWindowManagerTests.cpp create mode 100644 trview.app.ui.tests/StaticsWindowTests.cpp create mode 100644 trview.app.ui.tests/StaticsWindowTests.h create mode 100644 trview.app/Mocks/Windows/IStaticsWindow.h create mode 100644 trview.app/Mocks/Windows/IStaticsWindowManager.h create mode 100644 trview.app/Windows/Statics/IStaticsWindow.h create mode 100644 trview.app/Windows/Statics/IStaticsWindowManager.h create mode 100644 trview.app/Windows/Statics/StaticsWindow.cpp create mode 100644 trview.app/Windows/Statics/StaticsWindow.h create mode 100644 trview.app/Windows/Statics/StaticsWindowManager.cpp create mode 100644 trview.app/Windows/Statics/StaticsWindowManager.h diff --git a/README.md b/README.md index 051396b20..ce8954a90 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ CTRL + M | New Rooms window CTRL + L | New Lights window CTRL + K | New Camera/Sink window CTRL + P | New Plugins window +CTRL + S | New Statics Window CTRL + H | Toggle room lighting F1 | Toggle settings window F5 | Reload current level diff --git a/doc/lua/staticmesh.md b/doc/lua/staticmesh.md index 07edadf1b..666fcf961 100644 --- a/doc/lua/staticmesh.md +++ b/doc/lua/staticmesh.md @@ -4,10 +4,12 @@ | Name | Type | Mode | Description | | ---- | ---- | ---- | ---- | +| breakable | boolean | R | Whether this is a breakable static | | collision | [BoundingBox](boundingbox.md) | R | Collision bounding box | | id | number | R | Mesh ID or sprite texture ID | | position | [Vector3](vector3.md) | R | Static mesh position in the world. | | room | [Room](room.md) | R | Room that contains the static mesh | | rotation | number | R | Static mesh rotation | | type | string | R | Type of static mesh (Mesh/Sprite) | +| visible| boolean | RW | Whether the item is visible in the viewer | | visibility | [BoundingBox](boundingbox.md) | R | Visibility bounding box | diff --git a/trview.app.tests/ApplicationTests.cpp b/trview.app.tests/ApplicationTests.cpp index 37ba24820..18453d16b 100644 --- a/trview.app.tests/ApplicationTests.cpp +++ b/trview.app.tests/ApplicationTests.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include using namespace trview; using namespace trview::tests; @@ -87,6 +89,7 @@ namespace std::unique_ptr plugins_window_manager{ mock_unique() }; IRandomizerRoute::Source randomizer_route_source { [](auto&&...) { return mock_shared(); } }; std::shared_ptr fonts { mock_shared() }; + std::unique_ptr statics_window_manager{ mock_unique() }; std::unique_ptr build() { @@ -96,7 +99,7 @@ namespace std::move(items_window_manager), std::move(triggers_window_manager), std::move(route_window_manager), std::move(rooms_window_manager), level_source, startup_options, dialogs, files, std::move(imgui_backend), std::move(lights_window_manager), std::move(log_window_manager), std::move(textures_window_manager), std::move(camera_sink_window_manager), std::move(console_manager), - plugins, std::move(plugins_window_manager), randomizer_route_source, fonts); + plugins, std::move(plugins_window_manager), randomizer_route_source, fonts, std::move(statics_window_manager)); } test_module& with_dialogs(std::shared_ptr dialogs) @@ -237,6 +240,12 @@ namespace return *this; } + test_module& with_statics_window_manager(std::unique_ptr statics_window_manager) + { + this->statics_window_manager = std::move(statics_window_manager); + return *this; + } + test_module& with_fonts(std::shared_ptr fonts) { this->fonts = fonts; @@ -310,6 +319,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded) auto [lights_window_manager_ptr, lights_window_manager] = create_mock(); auto [textures_window_manager_ptr, textures_window_manager] = create_mock(); auto [camera_sink_window_manager_ptr, camera_sink_window_manager] = create_mock(); + auto [statics_window_manager_ptr, statics_window_manager] = create_mock(); auto route = mock_shared(); std::vector events; @@ -330,6 +340,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded) EXPECT_CALL(route_window_manager, set_route(A&>())).Times(3).WillRepeatedly([&](auto) { events.push_back("route_route"); }); EXPECT_CALL(lights_window_manager, set_lights(A>&>())).Times(1).WillOnce([&](auto) { events.push_back("lights_lights"); }); EXPECT_CALL(camera_sink_window_manager, set_camera_sinks).Times(1).WillOnce([&](auto) { events.push_back("camera_sinks_camera_sinks"); }); + EXPECT_CALL(statics_window_manager, set_statics).Times(1).WillOnce([&](auto) { events.push_back("statics_statics"); }); EXPECT_CALL(*route, clear()).Times(1).WillOnce([&] { events.push_back("route_clear"); }); EXPECT_CALL(*route, set_unsaved(false)).Times(1); EXPECT_CALL(textures_window_manager, set_texture_storage).Times(1).WillOnce([&](auto) { events.push_back("textures"); }); @@ -346,6 +357,7 @@ TEST(Application, WindowContentsResetBeforeViewerLoaded) .with_lights_window_manager(std::move(lights_window_manager_ptr)) .with_textures_window_manager(std::move(textures_window_manager_ptr)) .with_camera_sink_window_manager(std::move(camera_sink_window_manager_ptr)) + .with_statics_window_manager(std::move(statics_window_manager_ptr)) .build(); application->open("test_path.tr2", ILevel::OpenMode::Full); @@ -525,6 +537,8 @@ TEST(Application, WindowManagersUpdated) EXPECT_CALL(triggers_window_manager, update).Times(1); auto [lights_window_manager_ptr, lights_window_manager] = create_mock(); EXPECT_CALL(lights_window_manager, update).Times(1); + auto [statics_window_manager_ptr, statics_window_manager] = create_mock(); + EXPECT_CALL(statics_window_manager, update).Times(1); auto application = register_test_module() .with_route_window_manager(std::move(route_window_manager_ptr)) @@ -532,6 +546,7 @@ TEST(Application, WindowManagersUpdated) .with_rooms_window_manager(std::move(rooms_window_manager_ptr)) .with_triggers_window_manager(std::move(triggers_window_manager_ptr)) .with_lights_window_manager(std::move(lights_window_manager_ptr)) + .with_statics_window_manager(std::move(statics_window_manager_ptr)) .build(); application->render(); } @@ -558,6 +573,8 @@ TEST(Application, WindowManagersAndViewerRendered) EXPECT_CALL(console_manager, render).Times(1); auto [plugins_window_manager_ptr, plugins_window_manager] = create_mock(); EXPECT_CALL(plugins_window_manager, render).Times(1); + auto [statics_window_manager_ptr, statics_window_manager] = create_mock(); + EXPECT_CALL(statics_window_manager, render).Times(1); auto plugins = mock_shared(); EXPECT_CALL(*plugins, render_ui).Times(1); @@ -575,6 +592,7 @@ TEST(Application, WindowManagersAndViewerRendered) .with_camera_sink_window_manager(std::move(camera_sink_window_manager_ptr)) .with_console_manager(std::move(console_manager_ptr)) .with_plugins_window_manager(std::move(plugins_window_manager_ptr)) + .with_statics_window_manager(std::move(statics_window_manager_ptr)) .with_viewer(std::move(viewer_ptr)) .with_plugins(plugins) .build(); @@ -1307,14 +1325,17 @@ TEST(Application, OnStaticMeshSelected) auto level = mock_shared(); auto static_mesh = mock_shared(); auto [rooms_window_manager_ptr, rooms_window_manager] = create_mock(); + auto [statics_window_manager_ptr, statics_window_manager] = create_mock(); auto [viewer_ptr, viewer] = create_mock(); auto application = register_test_module() .with_rooms_window_manager(std::move(rooms_window_manager_ptr)) + .with_statics_window_manager(std::move(statics_window_manager_ptr)) .with_viewer(std::move(viewer_ptr)) .build(); application->set_current_level(level, ILevel::OpenMode::Full, false); EXPECT_CALL(viewer, select_static_mesh).Times(1); + EXPECT_CALL(statics_window_manager, select_static).Times(1); rooms_window_manager.on_static_mesh_selected(static_mesh); } diff --git a/trview.app.tests/Elements/LevelTests.cpp b/trview.app.tests/Elements/LevelTests.cpp index 1567ccf18..c13447baa 100644 --- a/trview.app.tests/Elements/LevelTests.cpp +++ b/trview.app.tests/Elements/LevelTests.cpp @@ -829,4 +829,28 @@ TEST(Level, SkidooGenerated) ASSERT_EQ(entities.size(), 2); ASSERT_EQ(entities[0].TypeID, 52); ASSERT_EQ(entities[1].TypeID, 51); -} \ No newline at end of file +} + +TEST(Level, StaticMeshChangingRaisesLevelChangedEvent) +{ + auto static_mesh = mock_shared(); + + auto room = mock_shared(); + ON_CALL(*room, static_meshes).WillByDefault(Return(std::vector>{ static_mesh })); + + auto [mock_level_ptr, mock_level] = create_mock(); + EXPECT_CALL(mock_level, num_rooms()).WillRepeatedly(Return(1)); + + auto level = register_test_module().with_level(std::move(mock_level_ptr)) + .with_room_source( + [&](auto&&...) + { + return room; + }).build(); + + uint32_t times_called = 0; + auto token = level->on_level_changed += [&](auto&&...) { ++times_called; }; + + static_mesh->on_changed(); + ASSERT_EQ(times_called, 1u); +} diff --git a/trview.app.tests/Elements/RoomTests.cpp b/trview.app.tests/Elements/RoomTests.cpp index 06f7c2da3..5d85343e4 100644 --- a/trview.app.tests/Elements/RoomTests.cpp +++ b/trview.app.tests/Elements/RoomTests.cpp @@ -678,4 +678,38 @@ TEST(Room, Sector) sector = room->sector(2, 2); s = sector.lock(); ASSERT_FALSE(s); -} \ No newline at end of file +} + +TEST(Room, PickTestsStaticMesh) +{ + using namespace DirectX; + using namespace DirectX::SimpleMath; + + auto level = mock_shared(); + ON_CALL(*level, get_static_mesh).WillByDefault(testing::Return(trlevel::tr_staticmesh{})); + + trlevel::tr3_room level_room{ .static_meshes = { {} } }; + + auto static_mesh = mock_shared(); + ON_CALL(*static_mesh, number).WillByDefault(Return(10)); + ON_CALL(*static_mesh, visible).WillByDefault(Return(true)); + EXPECT_CALL(*static_mesh, pick).Times(1).WillOnce(Return(PickResult{ true, 0, {}, {}, PickResult::Type::StaticMesh, 10 })); + + auto static_mesh_source = [&](auto&&...) + { + return static_mesh; + }; + + auto room = register_test_module() + .with_room(level_room) + .with_static_mesh_source(static_mesh_source) + .with_tr_level(level) + .build(); + + auto results = room->pick(Vector3(0, 0, -2), Vector3(0, 0, 1), PickFilter::StaticMeshes); + ASSERT_EQ(results.size(), 1); + auto result = results.front(); + ASSERT_EQ(result.hit, true); + ASSERT_EQ(result.type, PickResult::Type::StaticMesh); + ASSERT_EQ(result.index, 10); +} diff --git a/trview.app.tests/Elements/StaticMeshTests.cpp b/trview.app.tests/Elements/StaticMeshTests.cpp index 7fa2a507d..7b9bf87b0 100644 --- a/trview.app.tests/Elements/StaticMeshTests.cpp +++ b/trview.app.tests/Elements/StaticMeshTests.cpp @@ -19,3 +19,12 @@ TEST(StaticMesh, BoundingBoxRendered) StaticMesh mesh({}, {}, actual_mesh, {}, bounding_mesh); mesh.render_bounding_box(NiceMock{}, NiceMock{}, Colour::White); } + +TEST(StaticMesh, OnChangedRaised) +{ + StaticMesh mesh({}, {}, mock_shared(), {}, mock_shared()); + bool raised = false; + auto token = mesh.on_changed += [&] (){ raised = true; }; + mesh.set_visible(false); + ASSERT_EQ(raised, true); +} diff --git a/trview.app.tests/Lua/Elements/Lua_StaticMeshTests.cpp b/trview.app.tests/Lua/Elements/Lua_StaticMeshTests.cpp index 376f4aadd..3af92dd0f 100644 --- a/trview.app.tests/Lua/Elements/Lua_StaticMeshTests.cpp +++ b/trview.app.tests/Lua/Elements/Lua_StaticMeshTests.cpp @@ -12,6 +12,20 @@ using namespace trview::tests; using namespace testing; using namespace DirectX::SimpleMath; +TEST(Lua_StaticMesh, Breakable) +{ + auto mesh = mock_shared(); + EXPECT_CALL(*mesh, breakable).WillOnce(Return(true)); + + LuaState L; + lua::create_static_mesh(L, mesh); + lua_setglobal(L, "s"); + + ASSERT_EQ(0, luaL_dostring(L, "return s.breakable")); + ASSERT_EQ(LUA_TBOOLEAN, lua_type(L, -1)); + ASSERT_EQ(true, lua_toboolean(L, -1)); +} + TEST(Lua_StaticMesh, Collision) { auto static_mesh = mock_shared(); @@ -115,6 +129,18 @@ TEST(Lua_StaticMesh, Rotation) ASSERT_EQ(123.0f, lua_tonumber(L, -1)); } +TEST(Lua_StaticMesh, SetVisible) +{ + auto static_mesh = mock_shared()->with_number(100); + EXPECT_CALL(*static_mesh, set_visible(true)).Times(1); + + LuaState L; + lua::create_static_mesh(L, static_mesh); + lua_setglobal(L, "s"); + + ASSERT_EQ(0, luaL_dostring(L, "s.visible = true")); +} + TEST(Lua_StaticMesh, Type) { auto static_mesh = mock_shared(); @@ -134,6 +160,20 @@ TEST(Lua_StaticMesh, Type) ASSERT_STREQ("Sprite", lua_tostring(L, -1)); } +TEST(Lua_StaticMesh, Visible) +{ + auto static_mesh = mock_shared(); + EXPECT_CALL(*static_mesh, visible).WillOnce(Return(true)); + + LuaState L; + lua::create_static_mesh(L, static_mesh); + lua_setglobal(L, "s"); + + ASSERT_EQ(0, luaL_dostring(L, "return s.visible")); + ASSERT_EQ(LUA_TBOOLEAN, lua_type(L, -1)); + ASSERT_EQ(true, lua_toboolean(L, -1)); +} + TEST(Lua_StaticMesh, Visibility) { auto static_mesh = mock_shared(); diff --git a/trview.app.tests/PluginsWindowManagerTests.cpp b/trview.app.tests/PluginsWindowManagerTests.cpp index fc5d4881e..ce975d463 100644 --- a/trview.app.tests/PluginsWindowManagerTests.cpp +++ b/trview.app.tests/PluginsWindowManagerTests.cpp @@ -81,7 +81,7 @@ TEST(PluginsWindowManager, RendersAllWindows) manager->render(); } -TEST(PluginsWindowManager, CreateLightsWindowKeyboardShortcut) +TEST(PluginsWindowManager, CreatePluginsWindowKeyboardShortcut) { auto shortcuts = mock_shared(); EXPECT_CALL(*shortcuts, add_shortcut(true, 'P')).Times(1).WillOnce([&](auto, auto) -> Event<>&{ return shortcut_handler; }); diff --git a/trview.app.tests/Settings/SettingsLoaderTests.cpp b/trview.app.tests/Settings/SettingsLoaderTests.cpp index 8c917bc45..af7ba742f 100644 --- a/trview.app.tests/Settings/SettingsLoaderTests.cpp +++ b/trview.app.tests/Settings/SettingsLoaderTests.cpp @@ -232,6 +232,31 @@ TEST(SettingsLoader, ItemsStartupSaved) EXPECT_THAT(output, HasSubstr("\"itemsstartup\":true")); } +TEST(SettingsLoader, StaticsStartupLoaded) +{ + auto loader = setup_setting("{\"statics_startup\":false}"); + auto settings = loader->load_user_settings(); + ASSERT_EQ(settings.statics_startup, false); + + auto loader_true = setup_setting("{\"statics_startup\":true}"); + auto settings_true = loader_true->load_user_settings(); + ASSERT_EQ(settings_true.statics_startup, true); +} + +TEST(SettingsLoader, StaticsStartupSaved) +{ + std::string output; + auto loader = setup_save_setting(output); + UserSettings settings; + settings.statics_startup = false; + loader->save_user_settings(settings); + EXPECT_THAT(output, HasSubstr("\"statics_startup\":false")); + + settings.statics_startup = true; + loader->save_user_settings(settings); + EXPECT_THAT(output, HasSubstr("\"statics_startup\":true")); +} + TEST(SettingsLoader, TriggersStartupLoaded) { auto loader = setup_setting("{\"triggerstartup\":false}"); diff --git a/trview.app.tests/UI/ViewerUITests.cpp b/trview.app.tests/UI/ViewerUITests.cpp index 98fe856af..ee121f5fb 100644 --- a/trview.app.tests/UI/ViewerUITests.cpp +++ b/trview.app.tests/UI/ViewerUITests.cpp @@ -496,4 +496,32 @@ TEST(ViewerUI, FontForwarded) ASSERT_EQ(raised->second.size, 100); } +TEST(ViewerUI, OnStaticsStartupEventRaised) +{ + auto [settings_window_ptr, settings_window] = create_mock(); + auto ui = register_test_module().with_settings_window(std::move(settings_window_ptr)).build(); + + std::optional settings; + auto token = ui->on_settings += [&](auto raised) + { + settings = raised; + }; + + settings_window.on_statics_startup(true); + + ASSERT_TRUE(settings); + ASSERT_TRUE(settings.value().statics_startup); +} + +TEST(ViewerUI, SetStaticsStartupUpdatesSettingsWindow) +{ + auto [settings_window_ptr, settings_window] = create_mock(); + EXPECT_CALL(settings_window, set_statics_startup(true)).Times(1); + + auto ui = register_test_module().with_settings_window(std::move(settings_window_ptr)).build(); + + UserSettings settings{}; + settings.statics_startup = true; + ui->set_settings(settings); +} diff --git a/trview.app.tests/Windows/StaticsWindowManagerTests.cpp b/trview.app.tests/Windows/StaticsWindowManagerTests.cpp new file mode 100644 index 000000000..b3787aae5 --- /dev/null +++ b/trview.app.tests/Windows/StaticsWindowManagerTests.cpp @@ -0,0 +1,89 @@ +#include +#include +#include + +using namespace trview; +using namespace trview::tests; +using namespace trview::mocks; + +namespace +{ + Event<> shortcut_handler; + + auto register_test_module() + { + struct test_module + { + Window window{ create_test_window(L"StaticsWindowManagerTests") }; + std::shared_ptr shortcuts{ mock_shared() }; + IStaticsWindow::Source window_source{ [](auto&&...) { return mock_shared(); } }; + + test_module& with_window_source(const IStaticsWindow::Source& source) + { + this->window_source = source; + return *this; + } + + test_module& with_shortcuts(const std::shared_ptr& shortcuts) + { + this->shortcuts = shortcuts; + return *this; + } + + test_module() + { + EXPECT_CALL(*shortcuts, add_shortcut).WillRepeatedly([&](auto, auto) -> Event<>&{ return shortcut_handler; }); + } + + std::unique_ptr build() + { + return std::make_unique(window, shortcuts, window_source); + } + }; + + return test_module{}; + } +} + +TEST(StaticsWindowManager, WindowCreatedOnCommand) +{ + auto window = mock_shared(); + bool raised = false; + auto manager = register_test_module() + .with_window_source([&]() { raised = true; return window; }) + .build(); + + manager->process_message(WM_COMMAND, MAKEWPARAM(ID_WINDOWS_STATICS, 0), 0); + + ASSERT_TRUE(raised); +} + +TEST(StaticsWindowManager, WindowCreated) +{ + auto window = mock_shared(); + EXPECT_CALL(*window, set_number(1)).Times(1); + + auto manager = register_test_module() + .with_window_source([&]() { return window; }) + .build(); + + auto created = manager->create_window().lock(); + ASSERT_EQ(created, window); +} + +TEST(StaticsWindowManager, RendersAllWindows) +{ + auto window = mock_shared(); + EXPECT_CALL(*window, render).Times(1); + + auto manager = register_test_module().with_window_source([&]() { return window; }).build(); + manager->create_window(); + manager->render(); +} + +TEST(StaticsWindowManager, CreateStaticsWindowKeyboardShortcut) +{ + auto shortcuts = mock_shared(); + EXPECT_CALL(*shortcuts, add_shortcut(true, 'S')).Times(1).WillOnce([&](auto, auto) -> Event<>&{ return shortcut_handler; }); + auto manager = register_test_module().with_shortcuts(shortcuts).build(); +} diff --git a/trview.app.tests/trview.app.tests.vcxproj b/trview.app.tests/trview.app.tests.vcxproj index 2f4459d9a..535522066 100644 --- a/trview.app.tests/trview.app.tests.vcxproj +++ b/trview.app.tests/trview.app.tests.vcxproj @@ -94,6 +94,7 @@ + diff --git a/trview.app.tests/trview.app.tests.vcxproj.filters b/trview.app.tests/trview.app.tests.vcxproj.filters index 9057b3eed..66c0a5df5 100644 --- a/trview.app.tests/trview.app.tests.vcxproj.filters +++ b/trview.app.tests/trview.app.tests.vcxproj.filters @@ -200,6 +200,9 @@ ImGui + + Windows + diff --git a/trview.app.ui.tests/SettingsWindowTests.cpp b/trview.app.ui.tests/SettingsWindowTests.cpp index 4fa6e76ab..0daddf16c 100644 --- a/trview.app.ui.tests/SettingsWindowTests.cpp +++ b/trview.app.ui.tests/SettingsWindowTests.cpp @@ -504,6 +504,29 @@ void register_settings_window_tests(ImGuiTestEngine* engine) IM_CHECK_EQ(received_value.value(), 0.5f); }); + test>(engine, "Settings Window", "Clicking Statics Window on Startup Raises Event", + [](ImGuiTestContext* ctx) { render(ctx->GetVars>()); }, + [](ImGuiTestContext* ctx) + { + auto& controls = ctx->GetVars>(); + controls.ptr = register_test_module().build(); + controls.ptr->toggle_visibility(); + + std::optional received_value; + auto token = controls.ptr->on_statics_startup += [&](bool value) + { + received_value = value; + }; + + ctx->SetRef("Settings"); + ctx->ItemClick("TabBar/General"); + IM_CHECK_EQ(ctx->ItemIsChecked("TabBar/General/Open Statics Window at startup"), false); + ctx->ItemCheck("TabBar/General/Open Statics Window at startup"); + IM_CHECK_EQ(ctx->ItemIsChecked("TabBar/General/Open Statics Window at startup"), true); + IM_CHECK_EQ(received_value.has_value(), true); + IM_CHECK_EQ(received_value.value(), true); + }); + test>(engine, "Settings Window", "Clicking Triggers Window on Startup Raises Event", [](ImGuiTestContext* ctx) { render(ctx->GetVars>()); }, [](ImGuiTestContext* ctx) @@ -957,6 +980,22 @@ void register_settings_window_tests(ImGuiTestEngine* engine) IM_CHECK_EQ(ItemText(ctx, ctx->ItemInfo("TabBar/Camera/Sensitivity")->ID), "0.500"); }); + test>(engine, "Settings Window", "Set Statics Window on Startup Updates Checkbox", + [](ImGuiTestContext* ctx) { render(ctx->GetVars>()); }, + [](ImGuiTestContext* ctx) + { + auto& controls = ctx->GetVars>(); + controls.ptr = register_test_module().build(); + controls.ptr->toggle_visibility(); + + ctx->SetRef("Settings"); + ctx->ItemClick("TabBar/General"); + IM_CHECK_EQ(ctx->ItemIsChecked("TabBar/General/Open Statics Window at startup"), false); + controls.ptr->set_statics_startup(true); + ctx->Yield(); + IM_CHECK_EQ(ctx->ItemIsChecked("TabBar/General/Open Statics Window at startup"), true); + }); + test>(engine, "Settings Window", "Set Triggers Window on Startup Updates Checkbox", [](ImGuiTestContext* ctx) { render(ctx->GetVars>()); }, [](ImGuiTestContext* ctx) diff --git a/trview.app.ui.tests/StaticsWindowTests.cpp b/trview.app.ui.tests/StaticsWindowTests.cpp new file mode 100644 index 000000000..469bb467e --- /dev/null +++ b/trview.app.ui.tests/StaticsWindowTests.cpp @@ -0,0 +1,177 @@ +#include "pch.h" +#include "StaticsWindowTests.h" +#include +#include +#include +#include + +#include +#include + +using namespace testing; +using namespace trview; +using namespace trview::mocks; +using namespace trview::tests; + +namespace +{ + auto register_test_module() + { + struct test_module + { + std::shared_ptr clipboard{ mock_shared() }; + + std::unique_ptr build() + { + return std::make_unique(clipboard); + } + + test_module& with_clipboard(const std::shared_ptr& clipboard) + { + this->clipboard = clipboard; + return *this; + } + }; + + return test_module{}; + } + + struct StaticsWindowContext + { + std::unique_ptr ptr; + std::vector> statics; + }; + + void render(StaticsWindowContext& context) + { + if (context.ptr) + { + context.ptr->render(); + } + } +} + +void register_statics_window_tests(ImGuiTestEngine* engine) +{ + test(engine, "Statics Window", "Statics List Filtered When Room Set and Track Room Enabled", + [](ImGuiTestContext* ctx) { render(ctx->GetVars()); }, + [](ImGuiTestContext* ctx) + { + auto& context = ctx->GetVars(); + context.ptr = register_test_module().build(); + + std::shared_ptr raised_static; + auto token = context.ptr->on_static_selected += [&raised_static](const auto& stat) { raised_static = stat.lock(); }; + + auto room_78 = mock_shared()->with_number(78); + + context.statics = + { + mock_shared()->with_number(0)->with_room(mock_shared()->with_number(55)), + mock_shared()->with_number(1)->with_room(room_78) + }; + context.ptr->set_statics({ std::from_range, context.statics }); + context.ptr->set_current_room(room_78); + + ctx->ItemClick("Statics 0/**/Track##track"); + ctx->ItemCheck("/**/Room"); + ctx->KeyPress(ImGuiKey_Escape); + ctx->ItemClick("Statics 0/**/1##1"); + + IM_CHECK_EQ(ctx->ItemExists("Statics 0/**/0##0"), false); + IM_CHECK_EQ(ctx->ItemExists("Statics 0/**/1##1"), true); + IM_CHECK_EQ(raised_static, context.statics[1]); + }); + + test(engine, "Statics Window", "Statics List Not Filtered When Room Set and Track Room Disabled", + [](ImGuiTestContext* ctx) { render(ctx->GetVars()); }, + [](ImGuiTestContext* ctx) + { + auto& context = ctx->GetVars(); + context.ptr = register_test_module().build(); + + std::shared_ptr raised_static; + auto token = context.ptr->on_static_selected += [&raised_static](const auto& stat) { raised_static = stat.lock(); }; + + auto room_78 = mock_shared()->with_number(78); + + context.statics = + { + mock_shared()->with_number(0)->with_room(mock_shared()->with_number(55)), + mock_shared()->with_number(1)->with_room(room_78) + }; + context.ptr->set_statics({ std::from_range, context.statics }); + context.ptr->set_current_room(room_78); + + ctx->ItemClick("Statics 0/**/0##0"); + + IM_CHECK_EQ(raised_static, context.statics[0]); + }); + + test(engine, "Statics Window", "Static Selected Not Raised When Sync Static Disabled", + [](ImGuiTestContext* ctx) { render(ctx->GetVars()); }, + [](ImGuiTestContext* ctx) + { + auto& context = ctx->GetVars(); + context.ptr = register_test_module().build(); + + std::shared_ptr raised_static; + auto token = context.ptr->on_static_selected += [&raised_static](const auto& stat) { raised_static = stat.lock(); }; + + context.statics = + { + mock_shared()->with_number(0), + mock_shared()->with_number(1) + }; + context.ptr->set_statics({ std::from_range, context.statics }); + + ctx->ItemUncheck("Statics 0/**/Sync"); + ctx->ItemClick("Statics 0/**/1##1"); + + IM_CHECK_EQ(raised_static, nullptr); + }); + + test(engine, "Statics Window", "Static Selected Raised When Sync Static Enabled", + [](ImGuiTestContext* ctx) { render(ctx->GetVars()); }, + [](ImGuiTestContext* ctx) + { + auto& context = ctx->GetVars(); + context.ptr = register_test_module().build(); + + std::shared_ptr raised_static; + auto token = context.ptr->on_static_selected += [&raised_static](const auto& stat) { raised_static = stat.lock(); }; + + context.statics = + { + mock_shared()->with_number(0), + mock_shared()->with_number(1) + }; + context.ptr->set_statics({ std::from_range, context.statics }); + + ctx->ItemUncheck("Statics 0/**/Sync"); + ctx->ItemClick("Statics 0/**/1##1"); + + IM_CHECK_EQ(raised_static, nullptr); + + const auto from_window = context.ptr->selected_static().lock(); + IM_CHECK_EQ(from_window, context.statics[1]); + }); + + test(engine, "Statics Window", "Static Visibility Changed", + [](ImGuiTestContext* ctx) { render(ctx->GetVars()); }, + [](ImGuiTestContext* ctx) + { + auto& context = ctx->GetVars(); + context.ptr = register_test_module().build(); + + bool static1_visible = true; + auto static1 = mock_shared()->with_number(1); + ON_CALL(*static1, visible).WillByDefault([&] { return static1_visible; }); + EXPECT_CALL(*static1, set_visible(false)).Times(1).WillRepeatedly([&](bool v) { static1_visible = v; }); + + context.statics = { mock_shared()->with_number(0)->with_visible(true), static1 }; + context.ptr->set_statics({ std::from_range, context.statics }); + + ctx->ItemCheck("Statics 0/**/##hide-1"); + }); +} diff --git a/trview.app.ui.tests/StaticsWindowTests.h b/trview.app.ui.tests/StaticsWindowTests.h new file mode 100644 index 000000000..d6bace9e6 --- /dev/null +++ b/trview.app.ui.tests/StaticsWindowTests.h @@ -0,0 +1,5 @@ +#pragma once + +struct ImGuiTestEngine; + +void register_statics_window_tests(ImGuiTestEngine* engine); diff --git a/trview.app.ui.tests/trview.app.ui.tests.cpp b/trview.app.ui.tests/trview.app.ui.tests.cpp index d6c62a36c..389b4bc6d 100644 --- a/trview.app.ui.tests/trview.app.ui.tests.cpp +++ b/trview.app.ui.tests/trview.app.ui.tests.cpp @@ -46,6 +46,12 @@ void ImGuiTrviewTestEngineHook_RenderedText(ImGuiContext* ctx, ImGuiID id, const int main() { + // Quit if no debugger so it stops running in test discovery. + if (!ImOsIsDebuggerPresent()) + { + return 0; + } + // Setup application backend ImGuiApp* app = ImGuiApp_ImplDefault_Create(); diff --git a/trview.app.ui.tests/trview.app.ui.tests.vcxproj b/trview.app.ui.tests/trview.app.ui.tests.vcxproj index 774b10e99..9fc13500f 100644 --- a/trview.app.ui.tests/trview.app.ui.tests.vcxproj +++ b/trview.app.ui.tests/trview.app.ui.tests.vcxproj @@ -174,6 +174,7 @@ + @@ -252,6 +253,7 @@ + diff --git a/trview.app.ui.tests/trview.app.ui.tests.vcxproj.filters b/trview.app.ui.tests/trview.app.ui.tests.vcxproj.filters index 7e96dfc65..c46542544 100644 --- a/trview.app.ui.tests/trview.app.ui.tests.vcxproj.filters +++ b/trview.app.ui.tests/trview.app.ui.tests.vcxproj.filters @@ -111,6 +111,9 @@ imgui + + Source Files + @@ -185,6 +188,9 @@ Header Files + + Header Files + diff --git a/trview.app.ui.tests/trview_tests.cpp b/trview.app.ui.tests/trview_tests.cpp index 94a073e40..91c13c081 100644 --- a/trview.app.ui.tests/trview_tests.cpp +++ b/trview.app.ui.tests/trview_tests.cpp @@ -16,6 +16,7 @@ #include "RoomsWindowTests.h" #include "RouteWindowTests.h" #include "SettingsWindowTests.h" +#include "StaticsWindowTests.h" #include "TexturesWindowTests.h" #include "TriggersWindowTests.h" #include "ViewOptionsTests.h" @@ -38,6 +39,7 @@ void register_trview_tests(ImGuiTestEngine* engine) register_rooms_window_tests(engine); register_route_window_tests(engine); register_settings_window_tests(engine); + register_statics_window_tests(engine); register_textures_window_tests(engine); register_triggers_window_tests(engine); register_view_options_tests(engine); diff --git a/trview.app/Application.cpp b/trview.app/Application.cpp index d55df8cb4..2a6709f72 100644 --- a/trview.app/Application.cpp +++ b/trview.app/Application.cpp @@ -65,14 +65,15 @@ namespace trview std::shared_ptr plugins, std::unique_ptr plugins_window_manager, const IRandomizerRoute::Source& randomizer_route_source, - std::shared_ptr fonts) + std::shared_ptr fonts, + std::unique_ptr statics_window_manager) : MessageHandler(application_window), _instance(GetModuleHandle(nullptr)), _file_menu(std::move(file_menu)), _update_checker(std::move(update_checker)), _view_menu(window()), _settings_loader(settings_loader), _trlevel_source(trlevel_source), _viewer(std::move(viewer)), _route_source(route_source), _shortcuts(shortcuts), _items_windows(std::move(items_window_manager)), _triggers_windows(std::move(triggers_window_manager)), _route_window(std::move(route_window_manager)), _rooms_windows(std::move(rooms_window_manager)), _level_source(level_source), _dialogs(dialogs), _files(files), _timer(default_time_source()), _imgui_backend(std::move(imgui_backend)), _lights_windows(std::move(lights_window_manager)), _log_windows(std::move(log_window_manager)), _textures_windows(std::move(textures_window_manager)), _camera_sink_windows(std::move(camera_sink_window_manager)), _console_manager(std::move(console_manager)), - _plugins(plugins), _plugins_windows(std::move(plugins_window_manager)), _randomizer_route_source(randomizer_route_source), _fonts(fonts) + _plugins(plugins), _plugins_windows(std::move(plugins_window_manager)), _randomizer_route_source(randomizer_route_source), _fonts(fonts), _statics_windows(std::move(statics_window_manager)) { SetWindowLongPtr(window(), GWLP_USERDATA, reinterpret_cast(_imgui_backend.get())); @@ -94,6 +95,7 @@ namespace trview setup_route_window(); setup_lights_windows(); setup_camera_sink_windows(); + setup_statics_window(); setup_viewer(*startup_options); _plugins->initialise(this); } @@ -261,6 +263,7 @@ namespace trview for (const auto& light : _level->lights()) { set_light_visibility(light, true); } for (const auto& room : _level->rooms()) { set_room_visibility(room, true); } for (const auto& camera_sink : _level->camera_sinks()) { set_camera_sink_visibility(camera_sink, true); } + for (const auto& static_mesh : _level->static_meshes()) { if (auto stat = static_mesh.lock()) { stat->set_visible(true); } }; }; } @@ -292,6 +295,7 @@ namespace trview } }; _token_store += _viewer->on_font += [this](auto&& name, auto&& font) { _new_font = { name, font }; }; + _token_store += _viewer->on_static_mesh_selected += [this](const auto& static_mesh) { select_static_mesh(static_mesh); }; _viewer->set_settings(_settings); @@ -498,6 +502,7 @@ namespace trview _triggers_windows->set_room(room); _lights_windows->set_room(room); _camera_sink_windows->set_room(room); + _statics_windows->set_room(room); } void Application::select_trigger(const std::weak_ptr& trigger) @@ -704,6 +709,7 @@ namespace trview _route_window->update(elapsed); _lights_windows->update(elapsed); _plugins_windows->update(elapsed); + _statics_windows->update(elapsed); _viewer->render(); @@ -733,6 +739,7 @@ namespace trview _camera_sink_windows->render(); _console_manager->render(); _plugins_windows->render(); + _statics_windows->render(); _plugins->render_ui(); ImGui::PopFont(); @@ -899,6 +906,15 @@ namespace trview _token_store += _camera_sink_windows->on_camera_sink_type_changed += [this]() { _viewer->set_scene_changed(); }; } + void Application::setup_statics_window() + { + if (_settings.statics_startup) + { + _statics_windows->create_window(); + } + _token_store += _statics_windows->on_static_selected += [this](const auto& stat) { select_static_mesh(stat); }; + } + void Application::save_window_placement() { WINDOWPLACEMENT placement{}; @@ -980,6 +996,7 @@ namespace trview _lights_windows->set_level_version(_level->version()); _lights_windows->set_lights(_level->lights()); _camera_sink_windows->set_camera_sinks(_level->camera_sinks()); + _statics_windows->set_statics(_level->static_meshes()); if (open_mode == ILevel::OpenMode::Full) { _route->clear(); @@ -1085,5 +1102,6 @@ namespace trview select_room(static_mesh_ptr->room()); _viewer->select_static_mesh(static_mesh_ptr); + _statics_windows->select_static(static_mesh_ptr); } } \ No newline at end of file diff --git a/trview.app/Application.h b/trview.app/Application.h index 76daa3e48..31589bcff 100644 --- a/trview.app/Application.h +++ b/trview.app/Application.h @@ -29,6 +29,7 @@ #include "Plugins/IPlugins.h" #include "Windows/Plugins/IPluginsWindowManager.h" #include "UI/Fonts/IFonts.h" +#include "Windows/Statics/IStaticsWindowManager.h" struct ImFont; @@ -77,7 +78,8 @@ namespace trview std::shared_ptr plugins, std::unique_ptr plugins_window_manager, const IRandomizerRoute::Source& randomizer_route_source, - std::shared_ptr fonts); + std::shared_ptr fonts, + std::unique_ptr statics_window_manager); virtual ~Application(); /// Attempt to open the specified level file. /// @param filename The level file to open. @@ -102,6 +104,7 @@ namespace trview void setup_route_window(); void setup_lights_windows(); void setup_camera_sink_windows(); + void setup_statics_window(); void setup_shortcuts(); // Entity manipulation void add_waypoint(const DirectX::SimpleMath::Vector3& position, const DirectX::SimpleMath::Vector3& normal, std::weak_ptr room, IWaypoint::Type type, uint32_t index); @@ -180,6 +183,7 @@ namespace trview std::unique_ptr _console_manager; std::shared_ptr _plugins; std::unique_ptr _plugins_windows; + std::unique_ptr _statics_windows; IRandomizerRoute::Source _randomizer_route_source; std::shared_ptr _fonts; diff --git a/trview.app/ApplicationCreate.cpp b/trview.app/ApplicationCreate.cpp index 9a69edc66..bf51fcda7 100644 --- a/trview.app/ApplicationCreate.cpp +++ b/trview.app/ApplicationCreate.cpp @@ -69,6 +69,8 @@ #include "Windows/Plugins/PluginsWindow.h" #include "Tools/Toolbar.h" #include "UI/Fonts/Fonts.h" +#include "Windows/Statics/StaticsWindowManager.h" +#include "Windows/Statics/StaticsWindow.h" namespace trview { @@ -323,6 +325,7 @@ namespace trview auto trlevel_source = [=](auto&& filename) { return std::make_shared(filename, files, decrypter, log); }; auto textures_window_source = [=]() { return std::make_shared(); }; auto console_source = [=]() { return std::make_shared(dialogs, plugins, fonts); }; + auto statics_window_source = [=]() { return std::make_shared(clipboard); }; return std::make_unique( window, @@ -350,6 +353,7 @@ namespace trview plugins, std::make_unique(window, shortcuts, plugins_window_source), randomizer_route_source, - fonts); + fonts, + std::make_unique(window, shortcuts, statics_window_source)); } } diff --git a/trview.app/Elements/ILevel.h b/trview.app/Elements/ILevel.h index d4cdd3138..85bcd4d2c 100644 --- a/trview.app/Elements/ILevel.h +++ b/trview.app/Elements/ILevel.h @@ -123,6 +123,7 @@ namespace trview virtual bool show_items() const = 0; virtual std::vector> static_meshes() const = 0; virtual std::shared_ptr texture_storage() const = 0; + virtual std::weak_ptr static_mesh(uint32_t index) const = 0; /// /// Get the trigger at the specific index. /// diff --git a/trview.app/Elements/IStaticMesh.h b/trview.app/Elements/IStaticMesh.h index 5831c395c..ce9650581 100644 --- a/trview.app/Elements/IStaticMesh.h +++ b/trview.app/Elements/IStaticMesh.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace trview { @@ -36,6 +37,14 @@ namespace trview virtual DirectX::BoundingBox collision() const = 0; virtual Type type() const = 0; virtual uint16_t id() const = 0; + virtual void set_number(uint32_t number) = 0; + virtual uint32_t number() const = 0; + virtual uint16_t flags() const = 0; + virtual bool breakable() const = 0; + virtual bool visible() const = 0; + virtual void set_visible(bool value) = 0; + + Event<> on_changed; }; constexpr std::string to_string(IStaticMesh::Type type) noexcept; diff --git a/trview.app/Elements/Level.cpp b/trview.app/Elements/Level.cpp index 1799a8b4d..a6118f375 100644 --- a/trview.app/Elements/Level.cpp +++ b/trview.app/Elements/Level.cpp @@ -1299,16 +1299,48 @@ namespace trview } apply_ocb_adjustment(); + record_static_meshes(); } - std::vector> Level::static_meshes() const + void Level::record_static_meshes() { std::vector> results; for (const auto& room : _rooms) { results.append_range(room->static_meshes()); } - return results; + + uint32_t index = 0; + for (auto& stat : results) + { + if (auto stat_ptr = stat.lock()) + { + _token_store += stat_ptr->on_changed += [this]() { content_changed(); }; + stat_ptr->set_number(index); + } + ++index; + } + _static_meshes = results; + } + + std::vector> Level::static_meshes() const + { + return _static_meshes; + } + + void Level::content_changed() + { + _regenerate_transparency = true; + on_level_changed(); + } + + std::weak_ptr Level::static_mesh(uint32_t index) const + { + if (index >= _static_meshes.size()) + { + return {}; + } + return _static_meshes[index]; } bool find_item_by_type_id(const ILevel& level, uint32_t type_id, std::weak_ptr& output_item) diff --git a/trview.app/Elements/Level.h b/trview.app/Elements/Level.h index 898608c81..ef9e70e89 100644 --- a/trview.app/Elements/Level.h +++ b/trview.app/Elements/Level.h @@ -113,6 +113,7 @@ namespace trview const ILight::Source& light_source, const ICameraSink::Source& camera_sink_source); std::vector> static_meshes() const override; + std::weak_ptr static_mesh(uint32_t index) const override; private: void generate_rooms(const trlevel::ILevel& level, const IRoom::Source& room_source, const IMeshStorage& mesh_storage); void generate_triggers(const ITrigger::Source& trigger_source); @@ -158,6 +159,8 @@ namespace trview void apply_ocb_adjustment(); void deduplicate_triangles(); void record_models(const trlevel::ILevel& level); + void record_static_meshes(); + void content_changed(); std::shared_ptr _device; std::vector> _rooms; @@ -165,6 +168,7 @@ namespace trview std::vector> _entities; std::vector> _lights; std::vector> _camera_sinks; + std::vector> _static_meshes; graphics::IShader* _vertex_shader; graphics::IShader* _pixel_shader; diff --git a/trview.app/Elements/Room.cpp b/trview.app/Elements/Room.cpp index 48c3b8d5a..78acf4489 100644 --- a/trview.app/Elements/Room.cpp +++ b/trview.app/Elements/Room.cpp @@ -184,11 +184,16 @@ namespace trview { for (const auto& static_mesh : _static_meshes) { + if (!static_mesh->visible()) + { + continue; + } + PickResult static_mesh_result = static_mesh->pick(position, direction); if (static_mesh_result.hit) { - static_mesh_result.type = PickResult::Type::Room; - static_mesh_result.index = _index; + static_mesh_result.type = PickResult::Type::StaticMesh; + static_mesh_result.index = static_mesh->number(); pick_results.push_back(static_mesh_result); } } @@ -250,7 +255,10 @@ namespace trview _mesh->render(_room_offset * camera.view_projection(), *_texture_storage, colour, 1.0f, Vector3::Zero, false, !has_flag(render_filter, RenderFilter::Lighting)); for (const auto& mesh : _static_meshes) { - mesh->render(camera, *_texture_storage, colour); + if (mesh->visible()) + { + mesh->render(camera, *_texture_storage, colour); + } } } } @@ -466,7 +474,10 @@ namespace trview for (const auto& static_mesh : _static_meshes) { - static_mesh->get_transparent_triangles(transparency, camera, colour); + if (static_mesh->visible()) + { + static_mesh->get_transparent_triangles(transparency, camera, colour); + } } } } diff --git a/trview.app/Elements/StaticMesh.cpp b/trview.app/Elements/StaticMesh.cpp index b1a873410..94fac1d78 100644 --- a/trview.app/Elements/StaticMesh.cpp +++ b/trview.app/Elements/StaticMesh.cpp @@ -32,13 +32,14 @@ namespace trview _rotation(static_mesh.rotation / 16384.0f * DirectX::XM_PIDIV2), _bounding_mesh(bounding_mesh), _room(room), - _mesh_texture_id(static_mesh.mesh_id) + _mesh_texture_id(static_mesh.mesh_id), + _flags(level_static_mesh.Flags) { _world = Matrix::CreateRotationY(_rotation) * Matrix::CreateTranslation(_position); } StaticMesh::StaticMesh(const trlevel::tr_room_sprite& room_sprite, const DirectX::SimpleMath::Vector3& position, const DirectX::SimpleMath::Matrix& scale, std::shared_ptr mesh, const std::weak_ptr& room) - : _position(position), _sprite_mesh(mesh), _rotation(0), _scale(scale), _room(room), _mesh_texture_id(room_sprite.texture) + : _position(position), _mesh(mesh), _rotation(0), _scale(scale), _room(room), _mesh_texture_id(room_sprite.texture), _type(Type::Sprite) { using namespace DirectX::SimpleMath; _world = Matrix::CreateRotationY(_rotation) * Matrix::CreateTranslation(_position); @@ -46,20 +47,16 @@ namespace trview void StaticMesh::render(const ICamera& camera, const ILevelTextureStorage& texture_storage, const DirectX::SimpleMath::Color& colour) { - if (_sprite_mesh) + if (_type == Type::Sprite) { - auto wvp = create_billboard(_position, Vector3(0, -0.5f, 0), _scale, camera) * camera.view_projection(); - _sprite_mesh->render(wvp, texture_storage, colour); - } - else - { - _mesh->render(_world * camera.view_projection(), texture_storage, colour); + _world = create_billboard(_position, Vector3(0, -0.5f, 0), _scale, camera); } + _mesh->render(_world * camera.view_projection(), texture_storage, colour); } void StaticMesh::render_bounding_box(const ICamera& camera, const ILevelTextureStorage& texture_storage, const DirectX::SimpleMath::Color& colour) { - if (!_sprite_mesh) + if (_type == Type::Mesh) { const auto size = (_collision.Extents * 2.0f) / trlevel::Scale; const auto adjust = _collision.Center / trlevel::Scale; @@ -70,31 +67,23 @@ namespace trview void StaticMesh::get_transparent_triangles(ITransparencyBuffer& transparency, const ICamera& camera, const DirectX::SimpleMath::Color& colour) { - if (_sprite_mesh) + if (_type == Type::Sprite) { - auto world = create_billboard(_position, Vector3(0, -0.5f, 0), _scale, camera); - for (const auto& triangle : _sprite_mesh->transparent_triangles()) - { - transparency.add(triangle.transform(world, colour)); - } + _world = create_billboard(_position, Vector3(0, -0.5f, 0), _scale, camera); } - else + + for (const auto& triangle : _mesh->transparent_triangles()) { - for (const auto& triangle : _mesh->transparent_triangles()) - { - transparency.add(triangle.transform(_world, colour)); - } + transparency.add(triangle.transform(_world, colour)); } } PickResult StaticMesh::pick(const DirectX::SimpleMath::Vector3& position, const DirectX::SimpleMath::Vector3& direction) const { - if (_sprite_mesh) - { - return {}; - } - - PickResult result = _mesh->pick(Vector3::Transform(position, _world.Invert()), direction); + const auto transform = _world.Invert(); + auto normal_direction = Vector3::TransformNormal(direction, transform); + normal_direction.Normalize(); + PickResult result = _mesh->pick(Vector3::Transform(position, transform), normal_direction); result.position = Vector3::Transform(result.position, _world); return result; } @@ -126,7 +115,7 @@ namespace trview IStaticMesh::Type StaticMesh::type() const { - return _mesh ? Type::Mesh : Type::Sprite; + return _type; } uint16_t StaticMesh::id() const @@ -134,6 +123,40 @@ namespace trview return _mesh_texture_id; } + void StaticMesh::set_number(uint32_t number) + { + _number = number; + } + + uint32_t StaticMesh::number() const + { + return _number; + } + + uint16_t StaticMesh::flags() const + { + return _flags; + } + + bool StaticMesh::breakable() const + { + return _mesh_texture_id >= 50 && _mesh_texture_id <= 69; + } + + bool StaticMesh::visible() const + { + return _visible; + } + + void StaticMesh::set_visible(bool value) + { + if (value != _visible) + { + _visible = value; + on_changed(); + } + } + uint32_t static_mesh_room(const std::shared_ptr& static_mesh) { if (!static_mesh) diff --git a/trview.app/Elements/StaticMesh.h b/trview.app/Elements/StaticMesh.h index 24a55392f..54881c6f1 100644 --- a/trview.app/Elements/StaticMesh.h +++ b/trview.app/Elements/StaticMesh.h @@ -22,6 +22,12 @@ namespace trview float rotation() const override; Type type() const override; uint16_t id() const override; + void set_number(uint32_t number) override; + uint32_t number() const override; + uint16_t flags() const override; + bool breakable() const override; + bool visible() const override; + void set_visible(bool value) override; private: float _rotation; DirectX::SimpleMath::Vector3 _position; @@ -29,10 +35,13 @@ namespace trview DirectX::BoundingBox _collision; DirectX::SimpleMath::Matrix _world; std::shared_ptr _mesh; - std::shared_ptr _sprite_mesh; std::shared_ptr _bounding_mesh; DirectX::SimpleMath::Matrix _scale; std::weak_ptr _room; uint16_t _mesh_texture_id{ 0u }; + uint32_t _number{ 0u }; + uint16_t _flags{ 0u }; + bool _visible{ true }; + Type _type{ Type::Mesh }; }; } diff --git a/trview.app/Geometry/IMesh.cpp b/trview.app/Geometry/IMesh.cpp index 5b4fadf48..2edd06c6f 100644 --- a/trview.app/Geometry/IMesh.cpp +++ b/trview.app/Geometry/IMesh.cpp @@ -54,7 +54,11 @@ namespace trview { vertices[2].pos, vertices[1].pos, vertices[3].pos, vertices[2].uv, vertices[1].uv, vertices[3].uv, tile, TransparentTriangle::Mode::Normal }, }; - std::vector collision_triangles; + std::vector collision_triangles + { + Triangle(vertices[0].pos, vertices[1].pos, vertices[2].pos), + Triangle(vertices[2].pos, vertices[1].pos, vertices[3].pos) + }; float object_width = static_cast(right - left) / trlevel::Scale_X; float object_height = static_cast(bottom - top) / trlevel::Scale_Y; diff --git a/trview.app/Geometry/PickResult.cpp b/trview.app/Geometry/PickResult.cpp index 04dd62d0d..020f06755 100644 --- a/trview.app/Geometry/PickResult.cpp +++ b/trview.app/Geometry/PickResult.cpp @@ -176,6 +176,14 @@ namespace trview } break; } + case PickResult::Type::StaticMesh: + { + if (const auto static_mesh = level.static_mesh(result.index).lock()) + { + stream << to_string(static_mesh->type()) << " " << result.index; + } + break; + } } return stream.str(); diff --git a/trview.app/Lua/Elements/StaticMesh/Lua_StaticMesh.cpp b/trview.app/Lua/Elements/StaticMesh/Lua_StaticMesh.cpp index bfc668718..23ef0cd88 100644 --- a/trview.app/Lua/Elements/StaticMesh/Lua_StaticMesh.cpp +++ b/trview.app/Lua/Elements/StaticMesh/Lua_StaticMesh.cpp @@ -14,7 +14,12 @@ namespace trview { auto static_mesh = lua::get_self(L); const std::string key = lua_tostring(L, 2); - if (key == "collision") + if (key == "breakable") + { + lua_pushboolean(L, static_mesh->breakable()); + return 1; + } + else if (key == "collision") { return create_bounding_box(L, static_mesh->collision()); } @@ -41,6 +46,11 @@ namespace trview lua_pushstring(L, to_string(static_mesh->type()).c_str()); return 1; } + else if (key == "visible") + { + lua_pushboolean(L, static_mesh->visible()); + return 1; + } else if (key == "visibility") { return create_bounding_box(L, static_mesh->visibility()); @@ -52,8 +62,11 @@ namespace trview { auto static_mesh = lua::get_self(L); const std::string key = lua_tostring(L, 2); - key; - static_mesh; + + if (key == "visible") + { + static_mesh->set_visible(lua_toboolean(L, -1)); + } return 0; } } diff --git a/trview.app/Mocks/Elements/ILevel.h b/trview.app/Mocks/Elements/ILevel.h index 5aa150f00..4050c6cec 100644 --- a/trview.app/Mocks/Elements/ILevel.h +++ b/trview.app/Mocks/Elements/ILevel.h @@ -76,6 +76,7 @@ namespace trview MOCK_METHOD(void, set_camera_sink_visibility, (uint32_t, bool), (override)); MOCK_METHOD(void, set_show_camera_sinks, (bool), (override)); MOCK_METHOD(std::optional, selected_camera_sink, (), (const, override)); + MOCK_METHOD(std::weak_ptr, static_mesh, (uint32_t), (const, override)); MOCK_METHOD(std::vector>, static_meshes, (), (const)); std::shared_ptr with_version(trlevel::LevelVersion version) diff --git a/trview.app/Mocks/Elements/IStaticMesh.h b/trview.app/Mocks/Elements/IStaticMesh.h index e82d72d57..dad4a63ec 100644 --- a/trview.app/Mocks/Elements/IStaticMesh.h +++ b/trview.app/Mocks/Elements/IStaticMesh.h @@ -6,7 +6,7 @@ namespace trview { namespace mocks { - struct MockStaticMesh : public IStaticMesh + struct MockStaticMesh : public IStaticMesh, public std::enable_shared_from_this { MockStaticMesh(); virtual ~MockStaticMesh(); @@ -21,6 +21,30 @@ namespace trview MOCK_METHOD(DirectX::BoundingBox, visibility, (), (const, override)); MOCK_METHOD(Type, type, (), (const, override)); MOCK_METHOD(uint16_t, id, (), (const, override)); + MOCK_METHOD(void, set_number, (uint32_t), (override)); + MOCK_METHOD(uint32_t, number, (), (const, override)); + MOCK_METHOD(uint16_t, flags, (), (const, override)); + MOCK_METHOD(bool, visible, (), (const, override)); + MOCK_METHOD(void, set_visible, (bool), (override)); + MOCK_METHOD(bool, breakable, (), (const, override)); + + std::shared_ptr with_number(uint32_t number) + { + ON_CALL(*this, number).WillByDefault(testing::Return(number)); + return shared_from_this(); + } + + std::shared_ptr with_visible(bool value) + { + ON_CALL(*this, visible).WillByDefault(testing::Return(value)); + return shared_from_this(); + } + + std::shared_ptr with_room(std::shared_ptr room) + { + ON_CALL(*this, room).WillByDefault(testing::Return(room)); + return shared_from_this(); + } }; } } diff --git a/trview.app/Mocks/Mocks.cpp b/trview.app/Mocks/Mocks.cpp index 21f34c9a7..8834c5fdf 100644 --- a/trview.app/Mocks/Mocks.cpp +++ b/trview.app/Mocks/Mocks.cpp @@ -61,6 +61,8 @@ #include "Mocks/Windows/IPluginsWindow.h" #include "Mocks/Windows/IPluginsWindowManager.h" #include "Mocks/Tools/IToolbar.h" +#include "Mocks/Windows/IStaticsWindow.h" +#include "Mocks/Windows/IStaticsWindowManager.h" namespace trview { @@ -243,6 +245,12 @@ namespace trview MockPluginsWindowManager::MockPluginsWindowManager() {} MockPluginsWindowManager::~MockPluginsWindowManager() {} + MockStaticsWindow::MockStaticsWindow() {} + MockStaticsWindow::~MockStaticsWindow() {} + + MockStaticsWindowManager::MockStaticsWindowManager() {} + MockStaticsWindowManager::~MockStaticsWindowManager() {} + MockToolbar::MockToolbar() {} MockToolbar::~MockToolbar() {} } diff --git a/trview.app/Mocks/UI/ISettingsWindow.h b/trview.app/Mocks/UI/ISettingsWindow.h index ed56f8358..d17e748ea 100644 --- a/trview.app/Mocks/UI/ISettingsWindow.h +++ b/trview.app/Mocks/UI/ISettingsWindow.h @@ -35,6 +35,7 @@ namespace trview MOCK_METHOD(void, set_fov, (float), (override)); MOCK_METHOD(void, set_camera_sink_startup, (bool), (override)); MOCK_METHOD(void, set_plugin_directories, (const std::vector&), (override)); + MOCK_METHOD(void, set_statics_startup, (bool), (override)); }; } } diff --git a/trview.app/Mocks/Windows/IStaticsWindow.h b/trview.app/Mocks/Windows/IStaticsWindow.h new file mode 100644 index 000000000..fa06549e9 --- /dev/null +++ b/trview.app/Mocks/Windows/IStaticsWindow.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../../Windows/Statics/IStaticsWindow.h" + +namespace trview +{ + namespace mocks + { + struct MockStaticsWindow : public IStaticsWindow + { + MockStaticsWindow(); + virtual ~MockStaticsWindow(); + MOCK_METHOD(void, render, (), (override)); + MOCK_METHOD(std::weak_ptr, selected_static, (), (const, override)); + MOCK_METHOD(void, set_current_room, (const std::weak_ptr&), (override)); + MOCK_METHOD(void, set_number, (int32_t), (override)); + MOCK_METHOD(void, set_selected_static, (const std::weak_ptr&), (override)); + MOCK_METHOD(void, set_statics, (const std::vector>&), (override)); + MOCK_METHOD(void, update, (float), (override)); + }; + } +} diff --git a/trview.app/Mocks/Windows/IStaticsWindowManager.h b/trview.app/Mocks/Windows/IStaticsWindowManager.h new file mode 100644 index 000000000..e06e28b01 --- /dev/null +++ b/trview.app/Mocks/Windows/IStaticsWindowManager.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../../Windows/Statics/IStaticsWindowManager.h" + +namespace trview +{ + namespace mocks + { + struct MockStaticsWindowManager : public IStaticsWindowManager + { + MockStaticsWindowManager(); + ~MockStaticsWindowManager(); + MOCK_METHOD(std::weak_ptr, create_window, (), (override)); + MOCK_METHOD(void, render, (), (override)); + MOCK_METHOD(void, select_static, (const std::weak_ptr&), (override)); + MOCK_METHOD(void, set_room, (const std::weak_ptr&), (override)); + MOCK_METHOD(void, set_statics, (const std::vector>&), (override)); + MOCK_METHOD(void, update, (float), (override)); + }; + } +} diff --git a/trview.app/Resources/resource.h b/trview.app/Resources/resource.h index 4cc8c6286..e7af9f2db 100644 --- a/trview.app/Resources/resource.h +++ b/trview.app/Resources/resource.h @@ -48,6 +48,7 @@ #define ID_WINDOWS_CONSOLE 33024 #define ID_WINDOWS_PLUGINS 33025 #define ID_WINDOWS_RESET_FONTS 33026 +#define ID_WINDOWS_STATICS 33027 // Next default values for new objects // diff --git a/trview.app/Resources/trview.app.rc b/trview.app/Resources/trview.app.rc index 65790aa9f..faaaa9a77 100644 --- a/trview.app/Resources/trview.app.rc +++ b/trview.app/Resources/trview.app.rc @@ -75,6 +75,7 @@ BEGIN MENUITEM "Textures" ID_WINDOWS_TEXTURES MENUITEM "Console\tF11" ID_WINDOWS_CONSOLE MENUITEM "Plugins\tCtrl+P" ID_WINDOWS_PLUGINS + MENUITEM "Statics\tCtrl+S" ID_WINDOWS_STATICS MENUITEM SEPARATOR MENUITEM "Reset Layout" ID_WINDOWS_RESET_LAYOUT MENUITEM "Reset Fonts" ID_WINDOWS_RESET_FONTS diff --git a/trview.app/Settings/SettingsLoader.cpp b/trview.app/Settings/SettingsLoader.cpp index a63e4767c..c2e9e40e8 100644 --- a/trview.app/Settings/SettingsLoader.cpp +++ b/trview.app/Settings/SettingsLoader.cpp @@ -105,6 +105,7 @@ namespace trview read_attribute(json, settings.plugin_directories, "plugin_directories"); read_attribute(json, settings.toggles, "toggles"); read_attribute(json, settings.fonts, "fonts"); + read_attribute(json, settings.statics_startup, "statics_startup"); settings.recent_files.resize(std::min(settings.recent_files.size(), settings.max_recent_files)); } @@ -172,6 +173,7 @@ namespace trview json["plugin_directories"] = settings.plugin_directories; json["toggles"] = settings.toggles; json["fonts"] = settings.fonts; + json["statics_startup"] = settings.statics_startup; _files->save_file(file_path, json.dump()); } catch (...) diff --git a/trview.app/Settings/UserSettings.cpp b/trview.app/Settings/UserSettings.cpp index 035e296d3..69d36f184 100644 --- a/trview.app/Settings/UserSettings.cpp +++ b/trview.app/Settings/UserSettings.cpp @@ -42,6 +42,7 @@ namespace trview route_startup == other.route_startup && fov == other.fov && camera_sink_startup == other.camera_sink_startup && + statics_startup == other.statics_startup && plugin_directories == other.plugin_directories; } } diff --git a/trview.app/Settings/UserSettings.h b/trview.app/Settings/UserSettings.h index 348e1ee71..69ab4d109 100644 --- a/trview.app/Settings/UserSettings.h +++ b/trview.app/Settings/UserSettings.h @@ -63,6 +63,7 @@ namespace trview { "Default", {.name = "Arial", .filename = "arial.ttf", .size = 12 } }, { "Console", {.name = "Consolas", .filename = "consola.ttf", .size = 12 } } }; + bool statics_startup{ false }; bool operator==(const UserSettings& other) const; }; diff --git a/trview.app/UI/ISettingsWindow.h b/trview.app/UI/ISettingsWindow.h index c6fa9a632..22c9d9d9a 100644 --- a/trview.app/UI/ISettingsWindow.h +++ b/trview.app/UI/ISettingsWindow.h @@ -81,6 +81,7 @@ namespace trview Event on_camera_sink_startup; Event> on_plugin_directories; Event on_font; + Event on_statics_startup; virtual void render() = 0; /// @@ -183,5 +184,6 @@ namespace trview /// Toggle the visibility of the settings window. /// virtual void toggle_visibility() = 0; + virtual void set_statics_startup(bool value) = 0; }; } diff --git a/trview.app/UI/SettingsWindow.cpp b/trview.app/UI/SettingsWindow.cpp index 09f56a43f..297485aa0 100644 --- a/trview.app/UI/SettingsWindow.cpp +++ b/trview.app/UI/SettingsWindow.cpp @@ -41,6 +41,7 @@ namespace trview checkbox(Names::rooms_startup, _rooms_startup, on_rooms_startup); checkbox(Names::route_startup, _route_startup, on_route_startup); checkbox(Names::camera_sink_startup, _camera_sink_startup, on_camera_sink_startup); + checkbox(Names::statics_startup, _statics_startup, on_statics_startup); checkbox(Names::randomizer_tools, _randomizer_tools, on_randomizer_tools); if (ImGui::InputInt(Names::max_recent_files.c_str(), &_max_recent_files)) { @@ -370,4 +371,9 @@ namespace trview { _plugin_directories = directories; } + + void SettingsWindow::set_statics_startup(bool value) + { + _statics_startup = value; + } } diff --git a/trview.app/UI/SettingsWindow.h b/trview.app/UI/SettingsWindow.h index fe0cd7a8e..7d68b6532 100644 --- a/trview.app/UI/SettingsWindow.h +++ b/trview.app/UI/SettingsWindow.h @@ -39,6 +39,7 @@ namespace trview static inline const std::string fov = "Camera FOV"; static inline const std::string camera_sink_startup = "Open Camera/Sink Window at startup"; static inline const std::string reset_fov = "Reset##Fov"; + static inline const std::string statics_startup = "Open Statics Window at startup"; }; explicit SettingsWindow(const std::shared_ptr& dialogs, const std::shared_ptr& shell, const std::shared_ptr& fonts); @@ -68,6 +69,7 @@ namespace trview virtual void set_fov(float value) override; virtual void set_camera_sink_startup(bool value) override; void set_plugin_directories(const std::vector& directories) override; + void set_statics_startup(bool value) override; private: std::shared_ptr _dialogs; std::shared_ptr _shell; @@ -97,5 +99,6 @@ namespace trview std::vector _plugin_directories; std::vector _all_fonts; std::shared_ptr _fonts; + bool _statics_startup{ false }; }; } diff --git a/trview.app/UI/ViewerUI.cpp b/trview.app/UI/ViewerUI.cpp index 7a04f3ff3..52512584b 100644 --- a/trview.app/UI/ViewerUI.cpp +++ b/trview.app/UI/ViewerUI.cpp @@ -152,6 +152,7 @@ namespace trview forward_setting(_settings_window->on_camera_fov, _settings.fov); forward_setting(_settings_window->on_camera_sink_startup, _settings.camera_sink_startup); forward_setting(_settings_window->on_plugin_directories, _settings.plugin_directories); + forward_setting(_settings_window->on_statics_startup, _settings.statics_startup); _settings_window->on_font += on_font; _camera_position = std::make_unique(); @@ -390,6 +391,7 @@ namespace trview _settings_window->set_fov(settings.fov); _settings_window->set_camera_sink_startup(settings.camera_sink_startup); _settings_window->set_plugin_directories(settings.plugin_directories); + _settings_window->set_statics_startup(settings.statics_startup); _camera_position->set_display_degrees(settings.camera_display_degrees); _map_renderer->set_colours(settings.map_colours); for (const auto& toggle : settings.toggles) diff --git a/trview.app/Windows/ColumnSizer.cpp b/trview.app/Windows/ColumnSizer.cpp index 5ec25445f..b7868f1f6 100644 --- a/trview.app/Windows/ColumnSizer.cpp +++ b/trview.app/Windows/ColumnSizer.cpp @@ -4,6 +4,11 @@ namespace trview { void ColumnSizer::measure(const std::string& value, uint32_t index) { + if (ImGui::GetCurrentContext() == nullptr) + { + return; + } + if (index >= _sizes.size()) { _sizes.resize(index + 1, 0.0f); diff --git a/trview.app/Windows/IViewer.h b/trview.app/Windows/IViewer.h index eef07e280..af030b10d 100644 --- a/trview.app/Windows/IViewer.h +++ b/trview.app/Windows/IViewer.h @@ -82,6 +82,8 @@ namespace trview Event on_font; + Event> on_static_mesh_selected; + virtual CameraMode camera_mode() const = 0; /// Render the viewer. diff --git a/trview.app/Windows/RoomsWindow.cpp b/trview.app/Windows/RoomsWindow.cpp index e47444b13..2a0ad7f16 100644 --- a/trview.app/Windows/RoomsWindow.cpp +++ b/trview.app/Windows/RoomsWindow.cpp @@ -1194,7 +1194,6 @@ namespace trview ImGui::TableSetupScrollFreeze(1, 1); ImGui::TableHeadersRow(); - uint32_t i = 0; for (const auto& static_mesh : _static_meshes) { if (auto static_mesh_ptr = static_mesh.lock()) @@ -1212,7 +1211,7 @@ namespace trview _scroll_to_static_mesh = false; } - if (ImGui::Selectable(std::format("{0}##{0}", i++).c_str(), &selected, ImGuiSelectableFlags_SpanAllColumns | static_cast(ImGuiSelectableFlags_SelectOnNav))) + if (ImGui::Selectable(std::format("{0}##{0}", static_mesh_ptr->number()).c_str(), &selected, ImGuiSelectableFlags_SpanAllColumns | static_cast(ImGuiSelectableFlags_SelectOnNav))) { scroller.fix_scroll(); _local_selected_static_mesh = static_mesh_ptr; diff --git a/trview.app/Windows/Statics/IStaticsWindow.h b/trview.app/Windows/Statics/IStaticsWindow.h new file mode 100644 index 000000000..9e53eddcd --- /dev/null +++ b/trview.app/Windows/Statics/IStaticsWindow.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../Elements/IStaticMesh.h" + +namespace trview +{ + struct IStaticsWindow + { + using Source = std::function()>; + virtual ~IStaticsWindow() = 0; + virtual void render() = 0; + virtual std::weak_ptr selected_static() const = 0; + virtual void set_current_room(const std::weak_ptr& room) = 0; + virtual void set_number(int32_t number) = 0; + virtual void set_selected_static(const std::weak_ptr& static_mesh) = 0; + virtual void set_statics(const std::vector>& statics) = 0; + virtual void update(float dt) = 0; + + Event<> on_window_closed; + Event> on_static_selected; + }; +} diff --git a/trview.app/Windows/Statics/IStaticsWindowManager.h b/trview.app/Windows/Statics/IStaticsWindowManager.h new file mode 100644 index 000000000..e3cc58780 --- /dev/null +++ b/trview.app/Windows/Statics/IStaticsWindowManager.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "IStaticsWindow.h" +#include "../../Elements/IStaticMesh.h" + +namespace trview +{ + struct IStaticsWindowManager + { + virtual ~IStaticsWindowManager() = 0; + virtual std::weak_ptr create_window() = 0; + virtual void render() = 0; + virtual void select_static(const std::weak_ptr& static_mesh) = 0; + virtual void set_room(const std::weak_ptr& room) = 0; + virtual void set_statics(const std::vector>& statics) = 0; + virtual void update(float dt) = 0; + + Event> on_static_selected; + }; +} \ No newline at end of file diff --git a/trview.app/Windows/Statics/StaticsWindow.cpp b/trview.app/Windows/Statics/StaticsWindow.cpp new file mode 100644 index 000000000..81cca78ec --- /dev/null +++ b/trview.app/Windows/Statics/StaticsWindow.cpp @@ -0,0 +1,288 @@ +#include "StaticsWindow.h" +#include "../RowCounter.h" +#include "../../trview_imgui.h" + +namespace trview +{ + IStaticsWindow::~IStaticsWindow() + { + } + + StaticsWindow::StaticsWindow(const std::shared_ptr& clipboard) + : _clipboard(clipboard) + { + setup_filters(); + } + + void StaticsWindow::render() + { + if (!render_statics_window()) + { + on_window_closed(); + return; + } + } + + bool StaticsWindow::render_statics_window() + { + bool stay_open = true; + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(540, 500)); + if (ImGui::Begin(_id.c_str(), &stay_open)) + { + render_statics_list(); + ImGui::SameLine(); + render_static_details(); + _force_sort = false; + } + ImGui::End(); + ImGui::PopStyleVar(); + return stay_open; + } + + void StaticsWindow::render_statics_list() + { + calculate_column_widths(); + if (ImGui::BeginChild(Names::statics_list_panel.c_str(), ImVec2(0, 0), ImGuiChildFlags_AutoResizeX, ImGuiWindowFlags_NoScrollbar)) + { + _filters.render(); + + ImGui::SameLine(); + _track.render(); + + ImGui::SameLine(); + bool sync_static = _sync_static; + if (ImGui::Checkbox(Names::sync_item.c_str(), &sync_static)) + { + set_sync_static(sync_static); + } + + RowCounter counter{ "statics", _all_statics.size() }; + if (ImGui::BeginTable(Names::statics_list.c_str(), 5, ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY, ImVec2(0, -counter.height()))) + { + ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed, _column_sizer.size(0)); + ImGui::TableSetupColumn("Room", ImGuiTableColumnFlags_WidthFixed, _column_sizer.size(1)); + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, _column_sizer.size(2)); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, _column_sizer.size(3)); + ImGui::TableSetupColumn("Hide", ImGuiTableColumnFlags_WidthFixed, _column_sizer.size(4)); + ImGui::TableSetupScrollFreeze(1, 1); + ImGui::TableHeadersRow(); + + imgui_sort_weak(_all_statics, + { + [](auto&& l, auto&& r) { return l.number() < r.number(); }, + [](auto&& l, auto&& r) { return std::tuple(static_mesh_room(l), l.number()) < std::tuple(static_mesh_room(r), r.number()); }, + [](auto&& l, auto&& r) { return l.id() < r.id(); }, + [](auto&& l, auto&& r) { return l.type() < r.type(); }, + [](auto&& l, auto&& r) { return std::tuple(l.visible(), l.number()) < std::tuple(r.visible(), r.number()); } + }, _force_sort); + + for (const auto& stat : _all_statics) + { + auto stat_ptr = stat.lock(); + if (!stat_ptr || ( _track.enabled() && stat_ptr->room().lock() != _current_room.lock() || !_filters.match(*stat_ptr))) + { + continue; + } + + counter.count(); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + auto selected_static_mesh = _selected_static_mesh.lock(); + bool selected = selected_static_mesh && selected_static_mesh == stat_ptr; + + ImGuiScroller scroller; + if (selected && _scroll_to_static) + { + scroller.scroll_to_item(); + _scroll_to_static = false; + } + + ImGui::SetNextItemAllowOverlap(); + if (ImGui::Selectable(std::format("{0}##{0}", stat_ptr->number()).c_str(), &selected, ImGuiSelectableFlags_SpanAllColumns | static_cast(ImGuiSelectableFlags_SelectOnNav))) + { + scroller.fix_scroll(); + + set_local_selected_static_mesh(stat); + if (_sync_static) + { + on_static_selected(stat); + } + _scroll_to_static = false; + } + + ImGui::TableNextColumn(); + ImGui::Text(std::to_string(static_mesh_room(*stat_ptr)).c_str()); + ImGui::TableNextColumn(); + ImGui::Text(std::to_string(stat_ptr->id()).c_str()); + ImGui::TableNextColumn(); + ImGui::Text(to_string(stat_ptr->type()).c_str()); + ImGui::TableNextColumn(); + bool hidden = !stat_ptr->visible(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Checkbox(std::format("##hide-{}", stat_ptr->number()).c_str(), &hidden)) + { + stat_ptr->set_visible(!hidden); + } + ImGui::PopStyleVar(); + } + ImGui::EndTable(); + counter.render(); + } + } + ImGui::EndChild(); + } + + void StaticsWindow::render_static_details() + { + if (ImGui::BeginChild(Names::details_panel.c_str(), ImVec2(), true)) + { + ImGui::Text("Static Details"); + if (ImGui::BeginTable(Names::static_stats.c_str(), 2, 0, ImVec2(-1, 150))) + { + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Value"); + ImGui::TableNextRow(); + + if (auto stat = _selected_static_mesh.lock()) + { + auto add_stat = [&](const std::string& name, T&& value) + { + const auto string_value = get_string(value); + ImGui::TableNextColumn(); + ImGui::Text(name.c_str()); + ImGui::TableNextColumn(); + ImGui::Text(string_value.c_str()); + if (ImGui::BeginPopupContextItem(name.c_str())) + { + if (ImGui::MenuItem("Copy")) + { + _clipboard->write(to_utf16(string_value)); + } + ImGui::EndPopup(); + } + }; + + auto position_text = [&stat]() + { + const auto pos = stat->position() * trlevel::Scale; + return std::format("{:.0f}, {:.0f}, {:.0f}", pos.x, pos.y, pos.z); + }; + + add_stat("#", stat->number()); + add_stat("Position", position_text()); + add_stat("ID", stat->id()); + add_stat("Room", static_mesh_room(stat)); + add_stat("Type", to_string(stat->type())); + add_stat("Rotation", DirectX::XMConvertToDegrees(stat->rotation())); + add_stat("Flags", format_binary(stat->flags())); + add_stat("Breakable", stat->breakable()); + } + + ImGui::EndTable(); + } + } + ImGui::EndChild(); + } + + std::weak_ptr StaticsWindow::selected_static() const + { + return _selected_static_mesh; + } + + void StaticsWindow::set_number(int32_t number) + { + _id = std::format("Statics {}", number); + } + + void StaticsWindow::update(float) + { + } + + void StaticsWindow::calculate_column_widths() + { + _column_sizer.reset(); + + _column_sizer.measure("#__", 0); + _column_sizer.measure("Room__", 1); + _column_sizer.measure("ID__", 2); + _column_sizer.measure("Type__", 3); + _column_sizer.measure("Hide______", 4); + + for (const auto& stat : _all_statics) + { + if (auto stat_ptr = stat.lock()) + { + _column_sizer.measure(std::format("{0}##{0}", stat_ptr->number()), 0); + _column_sizer.measure(std::to_string(static_mesh_room(*stat_ptr)), 1); + _column_sizer.measure(std::to_string(stat_ptr->id()), 2); + _column_sizer.measure(to_string(stat_ptr->type()), 3); + } + } + } + + void StaticsWindow::set_statics(const std::vector>& statics) + { + _all_statics = statics; + setup_filters(); + _force_sort = true; + calculate_column_widths(); + } + + void StaticsWindow::setup_filters() + { + _filters.clear_all_getters(); + + std::set available_types; + for (const auto& stat : _all_statics) + { + if (auto stat_ptr = stat.lock()) + { + available_types.insert(to_string(stat_ptr->type())); + } + } + _filters.add_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& stat) { return to_string(stat.type()); }); + _filters.add_getter("#", [](auto&& stat) { return static_cast(stat.number()); }); + _filters.add_getter("X", [](auto&& stat) { return stat.position().x * trlevel::Scale_X; }); + _filters.add_getter("Y", [](auto&& stat) { return stat.position().y * trlevel::Scale_Y; }); + _filters.add_getter("Z", [](auto&& stat) { return stat.position().z * trlevel::Scale_Z; }); + _filters.add_getter("Rotation", [](auto&& stat) { return static_cast(DirectX::XMConvertToDegrees(stat.rotation())); }); + _filters.add_getter("ID", [](auto&& stat) { return static_cast(stat.id()); }); + _filters.add_getter("Room", [](auto&& stat) { return static_cast(static_mesh_room(stat)); }); + _filters.add_getter("Breakable", [](auto&& item) { return item.breakable(); }); + _filters.add_getter("Flags", [](auto&& stat) { return format_binary(stat.flags()); }); + } + + void StaticsWindow::set_local_selected_static_mesh(std::weak_ptr static_mesh) + { + _selected_static_mesh = static_mesh; + _force_sort = true; + } + + void StaticsWindow::set_sync_static(bool value) + { + if (_sync_static != value) + { + _sync_static = value; + _scroll_to_static = true; + if (_sync_static && _global_selected_static.lock()) + { + set_selected_static(_global_selected_static); + } + } + } + + void StaticsWindow::set_current_room(const std::weak_ptr& room) + { + _current_room = room; + } + + void StaticsWindow::set_selected_static(const std::weak_ptr& static_mesh) + { + _global_selected_static = static_mesh; + if (_sync_static) + { + _scroll_to_static = true; + set_local_selected_static_mesh(static_mesh); + } + } +} diff --git a/trview.app/Windows/Statics/StaticsWindow.h b/trview.app/Windows/Statics/StaticsWindow.h new file mode 100644 index 000000000..4ab2d4635 --- /dev/null +++ b/trview.app/Windows/Statics/StaticsWindow.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include + +#include "IStaticsWindow.h" +#include "../ColumnSizer.h" +#include "../../Elements/IRoom.h" +#include "../../Elements/IStaticMesh.h" +#include "../../Filters/Filters.h" +#include "../../Track/Track.h" + +namespace trview +{ + class StaticsWindow final : public IStaticsWindow + { + public: + struct Names + { + static inline const std::string sync_item = "Sync"; + static inline const std::string statics_list = "##staticslist"; + static inline const std::string statics_list_panel = "Statics List"; + static inline const std::string details_panel = "Static Details"; + static inline const std::string static_stats = "##staticstats"; + }; + + explicit StaticsWindow(const std::shared_ptr& clipboard); + virtual ~StaticsWindow() = default; + void render() override; + std::weak_ptr selected_static() const override; + void set_current_room(const std::weak_ptr& room) override; + void set_number(int32_t number) override; + void set_selected_static(const std::weak_ptr& static_mesh) override; + void set_statics(const std::vector>& statics) override; + void update(float dt) override; + private: + bool render_statics_window(); + void render_statics_list(); + void render_static_details(); + void calculate_column_widths(); + void setup_filters(); + void set_local_selected_static_mesh(std::weak_ptr static_mesh); + void set_sync_static(bool value); + + std::string _id{ "Statics 0" }; + ColumnSizer _column_sizer; + std::vector> _all_statics; + bool _force_sort{ false }; + Filters _filters; + bool _scroll_to_static{ false }; + std::weak_ptr _selected_static_mesh; + bool _sync_static{ true }; + Track _track; + std::weak_ptr _current_room; + std::weak_ptr _global_selected_static; + std::shared_ptr _clipboard; + }; +} + diff --git a/trview.app/Windows/Statics/StaticsWindowManager.cpp b/trview.app/Windows/Statics/StaticsWindowManager.cpp new file mode 100644 index 000000000..a60d2d53f --- /dev/null +++ b/trview.app/Windows/Statics/StaticsWindowManager.cpp @@ -0,0 +1,68 @@ +#include "StaticsWindowManager.h" +#include "../../Resources/resource.h" + +namespace trview +{ + IStaticsWindowManager::~IStaticsWindowManager() + { + } + + StaticsWindowManager::StaticsWindowManager(const Window& window, const std::shared_ptr& shortcuts, const IStaticsWindow::Source& statics_window_source) + : MessageHandler(window), _source(statics_window_source) + { + _token_store += shortcuts->add_shortcut(true, 'S') += [&]() { create_window(); }; + } + + std::optional StaticsWindowManager::process_message(UINT message, WPARAM wParam, LPARAM) + { + if (message == WM_COMMAND && LOWORD(wParam) == ID_WINDOWS_STATICS) + { + create_window(); + } + return {}; + } + + void StaticsWindowManager::render() + { + WindowManager::render(); + } + + std::weak_ptr StaticsWindowManager::create_window() + { + auto window = _source(); + window->set_statics(_statics); + window->on_static_selected += on_static_selected; + return add_window(window); + } + + void StaticsWindowManager::update(float delta) + { + WindowManager::update(delta); + } + + void StaticsWindowManager::set_statics(const std::vector>& statics) + { + _statics = statics; + for (auto& window : _windows) + { + window.second->set_statics(statics); + } + } + + void StaticsWindowManager::set_room(const std::weak_ptr& room) + { + _current_room = room; + for (auto& window : _windows) + { + window.second->set_current_room(room); + } + } + + void StaticsWindowManager::select_static(const std::weak_ptr& static_mesh) + { + for (auto& window : _windows) + { + window.second->set_selected_static(static_mesh); + } + } +} diff --git a/trview.app/Windows/Statics/StaticsWindowManager.h b/trview.app/Windows/Statics/StaticsWindowManager.h new file mode 100644 index 000000000..4b5d51c13 --- /dev/null +++ b/trview.app/Windows/Statics/StaticsWindowManager.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "../WindowManager.h" +#include "IStaticsWindowManager.h" +#include "IStaticsWindow.h" + +namespace trview +{ + class StaticsWindowManager final : public IStaticsWindowManager, public WindowManager, public MessageHandler + { + public: + explicit StaticsWindowManager(const Window& window, const std::shared_ptr& shortcuts, const IStaticsWindow::Source& statics_window_source); + ~StaticsWindowManager() = default; + std::optional process_message(UINT message, WPARAM wParam, LPARAM lParam) override; + void render() override; + std::weak_ptr create_window() override; + void update(float delta) override; + void set_statics(const std::vector>& statics) override; + void set_room(const std::weak_ptr& room) override; + void select_static(const std::weak_ptr& static_mesh) override; + private: + IStaticsWindow::Source _source; + std::vector> _statics; + std::weak_ptr _current_room; + }; +} diff --git a/trview.app/Windows/Viewer.cpp b/trview.app/Windows/Viewer.cpp index 5c7c00882..da2351d6b 100644 --- a/trview.app/Windows/Viewer.cpp +++ b/trview.app/Windows/Viewer.cpp @@ -202,6 +202,13 @@ namespace trview { on_camera_sink_visibility(level->camera_sink(_context_pick.index), false); } + else if (_context_pick.type == PickResult::Type::StaticMesh) + { + if (auto mesh = level->static_mesh(_context_pick.index).lock()) + { + mesh->set_visible(false); + } + } } }; _token_store += _ui->on_orbit += [&]() @@ -563,7 +570,7 @@ namespace trview _ui->set_show_context_menu(true); _camera_input.reset(true); _ui->set_remove_waypoint_enabled(_current_pick.type == PickResult::Type::Waypoint); - _ui->set_hide_enabled(equals_any(_current_pick.type, PickResult::Type::Entity, PickResult::Type::Trigger, PickResult::Type::Light, PickResult::Type::Room, PickResult::Type::CameraSink)); + _ui->set_hide_enabled(equals_any(_current_pick.type, PickResult::Type::Entity, PickResult::Type::Trigger, PickResult::Type::Light, PickResult::Type::Room, PickResult::Type::CameraSink, PickResult::Type::StaticMesh)); _ui->set_mid_waypoint_enabled(_current_pick.type == PickResult::Type::Room && _current_pick.triangle.normal.y < 0); const auto level = _level.lock(); @@ -1337,6 +1344,14 @@ namespace trview } break; } + case PickResult::Type::StaticMesh: + { + if (level) + { + on_static_mesh_selected(level->static_mesh(pick.index)); + } + break; + } } } diff --git a/trview.app/trview.app.vcxproj b/trview.app/trview.app.vcxproj index 631fee19a..ba101dae4 100644 --- a/trview.app/trview.app.vcxproj +++ b/trview.app/trview.app.vcxproj @@ -86,6 +86,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -181,6 +183,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -394,6 +398,10 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + + + diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index b4541f86f..36f893b03 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -329,6 +329,12 @@ UI\Fonts + + Windows\Statics + + + Windows\Statics + @@ -1063,6 +1069,24 @@ Mocks\UI + + Windows\Statics + + + Windows\Statics + + + Windows\Statics + + + Windows\Statics + + + Mocks\Windows + + + Mocks\Windows + @@ -1254,6 +1278,9 @@ {68f08988-7dd5-4109-ba3d-337b37fb0567} + + {56d85abd-e0f6-45f4-9776-b734b80d4b45} +