/
Mr.Stalin
/
FOnline-Engine
Обзор
Документация
Войти
/
Mr.Stalin
/
FOnline-Engine
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Source/Tools/SparkParticleEditor.cpp
1 934 строки
70 KB
cvet
Code coverage (#203)
10 авг 2026, 15:25
Не верифицирован
10 авг 2026, 15:25
d73ed92
Код
Авторство
О чём код?
// __________ ___ ______ _ // / ____/ __ \____ / (_)___ ___ / ____/___ ____ _(_)___ ___ // / /_ / / / / __ \/ / / __ \/ _ \ / __/ / __ \/ __ `/ / __ \/ _ ` // / __/ / /_/ / / / / / / / / / __/ / /___/ / / / /_/ / / / / / __/ // /_/ \____/_/ /_/_/_/_/ /_/\___/ /_____/_/ /_/\__, /_/_/ /_/\___/ // /____/ // FOnline Engine // https://fonline.ru // https://github.com/cvet/fonline // // MIT License // // Copyright (c) 2006 - 2026, Anton Tsvetinskiy aka cvet <cvet@tut.by> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #include "SparkParticleEditor.h" #if FO_SPARK_PARTICLES #include "Application.h" #include "ImGuiStuff.h" #include "Mapper.h" #include "SparkExtension.h" #include "SpriteResource.h" #include "VisualParticles.h" FO_DISABLE_WARNINGS_PUSH() #include "SPARK.h" FO_DISABLE_WARNINGS_POP() FO_BEGIN_NAMESPACE template<size_t Size> static auto SparkInputBufferView(const array<char, Size>& buffer) -> string_view { FO_STACK_TRACE_ENTRY(); auto end = std::find(buffer.begin(), buffer.end(), '\0'); return {buffer.data(), numeric_cast<size_t>(std::distance(buffer.begin(), end))}; } static auto SparkPathContainsCaseInsensitive(string_view text, string_view filter) -> bool { FO_STACK_TRACE_ENTRY(); if (filter.empty()) { return true; } string normalized_text = strex(text).lower().str(); string normalized_filter = strex(filter).lower().str(); return normalized_text.find(normalized_filter) != string::npos; } class SparkParticleSubEditor final : public ParticleSubEditor { public: explicit SparkParticleSubEditor(ptr<MapperEngine> mapper) : _mapper {mapper} { FO_STACK_TRACE_ENTRY(); } ~SparkParticleSubEditor() override { FO_STACK_TRACE_ENTRY(); } void Shutdown() override { FO_STACK_TRACE_ENTRY(); _editors.clear(); } void ResetLayout() override { FO_STACK_TRACE_ENTRY(); _browserWindowVisible = false; for (auto& editor : _editors) { editor->Hide(); } } void OnFocusGained() override { FO_STACK_TRACE_ENTRY(); if (_resourcesIndexed) { RefreshResources(); } } void DrawMenuItem() override { FO_STACK_TRACE_ENTRY(); ImGui::MenuItem("SPARK particle editor", nullptr, &_browserWindowVisible); } void DrawWindows() override { FO_STACK_TRACE_ENTRY(); DrawBrowserWindow(); for (auto it = _editors.begin(); it != _editors.end();) { try { if (!(*it)->Draw()) { it = _editors.erase(it); } else { ++it; } } catch (const std::exception& ex) { ReportExceptionAndContinue(ex); ++it; } } } private: void DrawBrowserWindow() { FO_STACK_TRACE_ENTRY(); if (!_browserWindowVisible) { return; } ImGui::SetNextWindowPos({112.0f, 80.0f}, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({560.0f, 620.0f}, ImGuiCond_FirstUseEver); if (!ImGui::Begin("SPARK Particle Editor", &_browserWindowVisible, 0)) { ImGui::End(); return; } if (!_resourcesIndexed || ImGui::IsWindowAppearing()) { RefreshResources(); } ImGui::InputTextWithHint("##SparkParticleEditorFilter", "Search .spark sources...", _filterBuf.data(), _filterBuf.size()); ImGui::SameLine(); if (ImGui::Button("Refresh")) { RefreshResources(); } string_view particle_filter = SparkInputBufferView(_filterBuf); size_t visible_resource_count = std::ranges::count_if(_resourcePaths, [&](const string& particle_path) { return SparkPathContainsCaseInsensitive(particle_path, particle_filter); }); ImGui::Text("Sources: %d, open editors: %d", numeric_cast<int32_t>(visible_resource_count), numeric_cast<int32_t>(_editors.size())); if (ImGui::BeginChild("##SparkParticleEditorResources", {0.0f, 0.0f}, true)) { for (const string& particle_path : _resourcePaths) { if (!SparkPathContainsCaseInsensitive(particle_path, particle_filter)) { continue; } if (ImGui::Selectable(particle_path.c_str(), false)) { OpenEditor(particle_path); } } } ImGui::EndChild(); ImGui::End(); } void OpenEditor(string_view asset_path) { FO_STACK_TRACE_ENTRY(); auto existing_editor = std::ranges::find_if(_editors, [asset_path](const unique_ptr<SparkParticleEditor>& editor) { return editor->GetAssetPath() == asset_path; }); if (existing_editor != _editors.end()) { (*existing_editor)->BringToFront(); return; } _editors.emplace_back(SafeAlloc::MakeUnique<SparkParticleEditor>(asset_path, _mapper->Settings, &_mapper->MapsFileSys, &_mapper->Resources, [this](string_view saved_path) { (void)_mapper->Resources.ReindexDataSources(); _mapper->SprMngr.InvalidateSpriteResource(strex(saved_path).change_file_extension("spk")); _mapper->AddMess(strex("SPARK particle source saved: {}", saved_path)); })); } void RefreshResources() { FO_STACK_TRACE_ENTRY(); vector<string> resource_paths; FileCollection particle_files = _mapper->MapsFileSys.FilterFiles("spark"); resource_paths.reserve(particle_files.GetFilesCount()); for (const auto& particle_file : particle_files) { resource_paths.emplace_back(particle_file.GetPath()); } std::ranges::sort(resource_paths); resource_paths.erase(std::ranges::unique(resource_paths).begin(), resource_paths.end()); _resourcePaths = std::move(resource_paths); _resourcesIndexed = true; } ptr<MapperEngine> _mapper; array<char, 160> _filterBuf {}; vector<string> _resourcePaths {}; vector<unique_ptr<SparkParticleEditor>> _editors {}; bool _resourcesIndexed {}; bool _browserWindowVisible {}; }; auto CreateSparkParticleSubEditor(ptr<MapperEngine> mapper) -> unique_ptr<ParticleSubEditor> { FO_STACK_TRACE_ENTRY(); return SafeAlloc::MakeUnique<SparkParticleSubEditor>(mapper); } static auto CreateSparkParticleEditorTextureLoader(ptr<FileSystem> baked_resources, vector<unique_ptr<RenderTexture>>& loaded_textures) -> ParticleTextureLoader { FO_STACK_TRACE_ENTRY(); ptr<vector<unique_ptr<RenderTexture>>> loaded_textures_ptr {&loaded_textures}; return [baked_resources, loaded_textures_ptr](string_view path) mutable -> pair<nptr<RenderTexture>, frect32> { auto file = baked_resources->ReadFile(path); if (!file) { WriteLog("SPARK particle editor could not read sprite resource '{}'", path); return {nullptr, {}}; } auto reader = file.GetReader(); uint8_t magic = reader.GetUInt8(); FO_VERIFY_AND_THROW(magic == SPRITE_RESOURCE_MAGIC, "Sprite file header magic is invalid", magic); uint8_t version = reader.GetUInt8(); FO_VERIFY_AND_THROW(version == SPRITE_RESOURCE_VERSION, "Sprite file version is not supported", version); // Collection header: frame count, animation ticks, direction count (void)reader.GetLEUInt16(); (void)reader.GetLEUInt16(); (void)reader.GetUInt8(); // First frame of the first direction: shared-reference flag, then offset and size (void)reader.GetUInt8(); (void)reader.GetLEInt16(); (void)reader.GetLEInt16(); uint16_t w = reader.GetLEUInt16(); uint16_t h = reader.GetLEUInt16(); (void)reader.GetLEInt16(); (void)reader.GetLEInt16(); const_span<uint8_t> data = reader.GetCurDataSpan(numeric_cast<size_t>(w) * h * sizeof(ucolor)); FO_VERIFY_AND_THROW(!data.empty(), "Sprite has no pixel data"); auto tex = GetApp()->Render.CreateTexture({w, h}, true, false); nptr<const uint8_t> data_ptr = data.data(); const_span<ucolor> pixels {data_ptr.reinterpret_as<const ucolor>().get(), numeric_cast<size_t>(w) * h}; tex->UpdateTextureRegion({}, {w, h}, pixels); nptr<RenderTexture> texture = tex.as_nptr(); loaded_textures_ptr->emplace_back(std::move(tex)); return {texture, {0.0f, 0.0f, 1.0f, 1.0f}}; }; } static auto GetSparkParticleRuntimeSystem(ptr<ParticleSystem> particle) -> ptr<SparkParticleRuntimeSystem> { FO_STACK_TRACE_ENTRY(); auto runtime_system = particle->GetRuntimeSystem().dyn_cast<SparkParticleRuntimeSystem>(); FO_VERIFY_AND_THROW(runtime_system, "SPARK particle editor received a non-SPARK runtime system"); return runtime_system.as_ptr(); } struct SparkParticleEditor::Impl { Impl(string_view asset_path, ptr<GlobalSettings> settings, ptr<FileSystem> raw_resources, ptr<FileSystem> baked_resources); [[nodiscard]] auto GetEditedSparkSystem() -> SPK::Ref<SPK::System>; [[nodiscard]] auto GetSparkGroups() -> vector<SPK::Ref<SPK::Group>>; [[nodiscard]] auto GetSparkFallbackGroup(const SPK::Ref<SPK::Group>& preferred_group) -> SPK::Ref<SPK::Group>; [[nodiscard]] auto GetFirstSparkEmitter(const SPK::Ref<SPK::Group>& preferred_group) -> SPK::Ref<SPK::Emitter>; [[nodiscard]] auto GetSparkObjectLabel(const SPK::Ref<SPK::SPKObject>& obj, size_t index) -> string; // Generic void DrawGenericSparkObject(const SPK::Ref<SPK::SPKObject>& obj); // Core void DrawSparkTransformable(const SPK::Ref<SPK::Transformable>& obj); void DrawSparkObject(const SPK::Ref<SPK::System>& obj); void DrawSparkObject(const SPK::Ref<SPK::Group>& obj); // Interpolators void DrawSparkObject(const SPK::Ref<SPK::FloatDefaultInitializer>& obj); void DrawSparkObject(const SPK::Ref<SPK::ColorDefaultInitializer>& obj); void DrawSparkObject(const SPK::Ref<SPK::FloatRandomInitializer>& obj); void DrawSparkObject(const SPK::Ref<SPK::ColorRandomInitializer>& obj); void DrawSparkObject(const SPK::Ref<SPK::FloatSimpleInterpolator>& obj); void DrawSparkObject(const SPK::Ref<SPK::ColorSimpleInterpolator>& obj); void DrawSparkObject(const SPK::Ref<SPK::FloatRandomInterpolator>& obj); void DrawSparkObject(const SPK::Ref<SPK::ColorRandomInterpolator>& obj); void DrawSparkObject(const SPK::Ref<SPK::FloatGraphInterpolator>& obj); void DrawSparkObject(const SPK::Ref<SPK::ColorGraphInterpolator>& obj); // Zones void DrawSparkInnerZone(const char* name, const function<SPK::Ref<SPK::Zone>()>& get, const function<void(const SPK::Ref<SPK::Zone>&)>& set); void DrawSparkZone(const SPK::Ref<SPK::Zone>& obj); void DrawSparkObject(const SPK::Ref<SPK::Point>& obj); void DrawSparkObject(const SPK::Ref<SPK::Sphere>& obj); void DrawSparkObject(const SPK::Ref<SPK::Plane>& obj); void DrawSparkObject(const SPK::Ref<SPK::Ring>& obj); void DrawSparkObject(const SPK::Ref<SPK::Box>& obj); void DrawSparkObject(const SPK::Ref<SPK::Cylinder>& obj); // Emitters void DrawSparkEmitter(const SPK::Ref<SPK::Emitter>& obj); void DrawSparkObject(const SPK::Ref<SPK::StaticEmitter>& obj); void DrawSparkObject(const SPK::Ref<SPK::RandomEmitter>& obj); void DrawSparkObject(const SPK::Ref<SPK::StraightEmitter>& obj); void DrawSparkObject(const SPK::Ref<SPK::SphericEmitter>& obj); void DrawSparkObject(const SPK::Ref<SPK::NormalEmitter>& obj); // Modifiers void DrawSparkModifier(const SPK::Ref<SPK::Modifier>& obj); void DrawSparkZonedModifier(const SPK::Ref<SPK::ZonedModifier>& obj); void DrawSparkObject(const SPK::Ref<SPK::Gravity>& obj); void DrawSparkObject(const SPK::Ref<SPK::Friction>& obj); void DrawSparkObject(const SPK::Ref<SPK::Obstacle>& obj); void DrawSparkObject(const SPK::Ref<SPK::Rotator>& obj); void DrawSparkObject(const SPK::Ref<SPK::Collider>& obj); void DrawSparkObject(const SPK::Ref<SPK::Destroyer>& obj); void DrawSparkObject(const SPK::Ref<SPK::Vortex>& obj); void DrawSparkObject(const SPK::Ref<SPK::EmitterAttacher>& obj); void DrawSparkObject(const SPK::Ref<SPK::PointMass>& obj); void DrawSparkObject(const SPK::Ref<SPK::RandomForce>& obj); void DrawSparkObject(const SPK::Ref<SPK::LinearForce>& obj); // Actions void DrawSparkAction(const SPK::Ref<SPK::Action>& obj); void DrawSparkObject(const SPK::Ref<SPK::ActionSet>& obj); void DrawSparkObject(const SPK::Ref<SPK::SpawnParticlesAction>& obj); // Renderers void DrawSparkObject(const SPK::Ref<SPK::Renderer>& obj); // Helpers void DrawSparkArray(const char* label, bool opened, const function<size_t()>& get_size, const function<const SPK::Ref<SPK::SPKObject>(size_t)>& get, const function<void(size_t)>& del, const function<void()>& add_draw); void DrawSparkNullableField(const char* label, const function<SPK::Ref<SPK::SPKObject>()>& get, const function<void()>& del, const function<void()>& add_draw); void DrawSparkGroupRef(const char* label, const SPK::Ref<SPK::Group>& current, const function<void(const SPK::Ref<SPK::Group>&)>& set); void DrawSparkEmitterRef(const char* label, const SPK::Ref<SPK::Emitter>& current, const function<void(const SPK::Ref<SPK::Emitter>&)>& set); void DrawSparkActionAddButtons(const function<void(const SPK::Ref<SPK::Action>&)>& add, const SPK::Ref<SPK::Group>& preferred_group); unique_ptr<RenderTexture> RenderTarget; vector<unique_ptr<RenderTexture>> LoadedTextures {}; ptr<GlobalSettings> Settings; ptr<FileSystem> RawResources; EffectManager EffectMngr; GameTimer GameTime; ParticleManager ParticleMngr; optional<ParticleSystem> Particle; SPK::Ref<SPK::System> SystemBackup {}; bool AddingMode {true}; bool RemovingMode {true}; bool NamingMode {}; float32_t FloatGraphEntryPosition {}; float32_t ColorGraphEntryPosition {}; vector<string> AllEffects {}; vector<string> AllTextures {}; string LoadError {}; bool Changed {}; }; SparkParticleEditor::Impl::Impl(string_view asset_path, ptr<GlobalSettings> settings, ptr<FileSystem> raw_resources, ptr<FileSystem> baked_resources) : RenderTarget {GetApp()->Render.CreateTexture({200, 200}, true, true)}, Settings {settings}, RawResources {raw_resources}, EffectMngr {settings, baked_resources, &GetApp()->Render}, GameTime {settings}, ParticleMngr {settings, &EffectMngr, &GetApp()->Render, baked_resources, &GameTime, CreateSparkParticleEditorTextureLoader(baked_resources, LoadedTextures)}, Particle {ParticleMngr.CreateParticle(strex(asset_path).change_file_extension("spk"))} { FO_STACK_TRACE_ENTRY(); if (Particle.has_value()) { SystemBackup = SPK::SPKObject::copy(GetSparkParticleRuntimeSystem(&Particle.value())->GetEditableBaseSystem()); } else { LoadError = strex("Failed to load SPARK particle preview for '{}'", asset_path); WriteLog("SPARK particle editor: failed to load preview for '{}'", asset_path); } } SparkParticleEditor::SparkParticleEditor(SparkParticleEditor&&) noexcept = default; SparkParticleEditor::SparkParticleEditor(string_view asset_path, ptr<GlobalSettings> settings, ptr<FileSystem> raw_resources, ptr<FileSystem> baked_resources, function<void(string_view)> on_saved) : _impl {SafeAlloc::MakeUnique<Impl>(asset_path, settings, raw_resources, baked_resources)}, _assetPath {asset_path}, _windowTitle {strex("SPARK Particle Editor - {}###SparkParticleEditor_{}", asset_path, asset_path)}, _closePopupTitle {strex("Unsaved SPARK particle changes###SparkParticleEditorClose_{}", asset_path)}, _onSaved {std::move(on_saved)} { FO_STACK_TRACE_ENTRY(); auto fofx_files = raw_resources->FilterFiles("fofx"); for (const auto& file_header : fofx_files) { _impl->AllEffects.emplace_back(string(file_header.GetPath())); } auto tex_files = raw_resources->FilterFiles("tga", strex(asset_path).extract_dir()); for (const auto& file_header : tex_files) { _impl->AllTextures.emplace_back(string(file_header.GetPath().substr(strex(asset_path).extract_dir().length() + 1))); } } SparkParticleEditor::~SparkParticleEditor() = default; void SparkParticleEditor::BringToFront() noexcept { FO_STACK_TRACE_ENTRY(); _visible = true; _bringToFront = true; } auto SparkParticleEditor::Draw() -> bool { FO_STACK_TRACE_ENTRY(); if (!_visible) { return true; } ImGui::SetNextWindowPos({128.0f, 64.0f}, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize({760.0f, 760.0f}, ImGuiCond_FirstUseEver); if (_bringToFront) { ImGui::SetNextWindowFocus(); _bringToFront = false; } bool opened = true; if (ImGui::Begin(_windowTitle.c_str(), &opened, ImGuiWindowFlags_NoCollapse)) { DrawContent(); } bool keep_open = true; if (!opened && !_changed) { keep_open = false; } else if (!opened) { ImGui::OpenPopup(_closePopupTitle.c_str()); } if (ImGui::BeginPopupModal(_closePopupTitle.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::TextWrapped("Save changes to %s before closing?", _assetPath.c_str()); if (!_saveError.empty()) { ImGui::TextWrapped("%s", _saveError.c_str()); } if (ImGui::Button("Save and close")) { if (SaveChanges()) { keep_open = false; ImGui::CloseCurrentPopup(); } } ImGui::SameLine(); if (ImGui::Button("Discard and close")) { DiscardChanges(); keep_open = false; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { _saveError.clear(); ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::End(); return keep_open; } void SparkParticleEditor::DrawContent() { FO_STACK_TRACE_ENTRY(); _impl->GameTime.FrameAdvance(true); _impl->Changed = false; if (!_impl->Particle.has_value()) { ImGui::TextWrapped("%s", !_impl->LoadError.empty() ? _impl->LoadError.c_str() : "SPARK particle preview is unavailable"); return; } ParticleSpriteFrame frame = _impl->Particle->ComputeSpriteFrame(*_impl->Settings); int32_t draw_width = frame.DrawSize.width; int32_t draw_height = frame.DrawSize.height; bool can_save_source = strex(_assetPath).get_file_extension() == "spark"; if (ImGui::BeginChild("Info", {0.0f, numeric_cast<float32_t>(draw_height + 120)})) { ImGui::Checkbox("Adding mode", &_impl->AddingMode); ImGui::SameLine(); ImGui::Checkbox("Removing mode", &_impl->RemovingMode); ImGui::SameLine(); ImGui::Checkbox("Naming mode", &_impl->NamingMode); ImGui::Checkbox("Auto replay", &_autoReplay); ImGui::Text("Elapsed: %.2f", numeric_cast<float64_t>(_impl->Particle->GetElapsedTime())); ImGui::SameLine(); ImGui::SliderFloat("Dir angle", &_dirAngle, 0.0f, 360.0f); if (ImGui::Button("Respawn")) { _impl->Particle->Respawn(); } if (_changed) { ImGui::SameLine(); if (ImGui::Button("Discard")) { DiscardChanges(); } if (can_save_source) { ImGui::SameLine(); if (ImGui::Button("Save")) { (void)SaveChanges(); } } else { ImGui::SameLine(); ImGui::TextDisabled("Save is unavailable for this source format"); } if (!_saveError.empty()) { ImGui::TextWrapped("%s", _saveError.c_str()); } } } ImGui::EndChild(); if (ImGui::BeginChild("Hierarchy")) { _impl->DrawGenericSparkObject(GetSparkParticleRuntimeSystem(&_impl->Particle.value())->GetEditableBaseSystem()); } ImGui::EndChild(); _changed |= _impl->Changed; if (_impl->Changed) { _impl->Particle->Respawn(); } float32_t frame_width = numeric_cast<float32_t>(draw_width); float32_t frame_height = numeric_cast<float32_t>(draw_height); float32_t frame_ratio = frame_width / frame_height; float32_t proj_height = frame_height * (1.0f / _impl->Settings->ModelProjFactor); float32_t proj_width = proj_height * frame_ratio; mat44 proj = GetApp()->Render.CreateOrthoMatrix(0.0f, proj_width, 0.0f, proj_height, -10.0f, 10.0f); mat44 world = glm::translate(mat44 {1.0f}, vec3 {proj_width / 2.0f, proj_height / 4.0f, 0.0f}); vec3 pos_offest; pos_offest = vec3(); vec3 view_offset; view_offset = vec3(); _impl->Particle->Setup(proj, world, pos_offest, _dirAngle, view_offset); auto prev_rt = GetApp()->Render.GetRenderTarget(); GetApp()->Render.SetRenderTarget(_impl->RenderTarget); GetApp()->Render.ClearRenderTarget(ucolor::clear, true); _impl->Particle->Draw(); GetApp()->Render.SetRenderTarget(prev_rt); ptr<ImDrawList> draw_list = ImGui::GetWindowDrawList(); auto pos = ImGui::GetWindowPos(); pos.x += 120.0f; pos.y += 140.0f; auto border_col = ImGui::ColorConvertFloat4ToU32({1.0f, 0.0f, 0.0f, 1.0f}); draw_list->AddLine({pos.x + frame_width / 2.0f, pos.y}, {pos.x + frame_width / 2.0f, pos.y + frame_height}, border_col); draw_list->AddLine({pos.x, pos.y + frame_height - frame_height / 4.0f}, {pos.x + frame_width, pos.y + frame_height - frame_height / 4.0f}, border_col); if (GetApp()->Render.IsRenderTargetFlipped()) { draw_list->AddImage(_impl->RenderTarget.get(), pos, {pos.x + frame_width, pos.y + frame_height}, {0.0f, 1.0f}, {1.0f, 0.0f}); } else { draw_list->AddImage(_impl->RenderTarget.get(), pos, {pos.x + frame_width, pos.y + frame_height}, {0.0f, 0.0f}, {1.0f, 1.0f}); } draw_list->AddRect({pos.x - 1.0f, pos.y - 1.0f}, {pos.x + frame_width + 2.0f, pos.y + frame_height + 2.0f}, border_col); if (!_impl->Particle->IsActive() && _autoReplay) { _impl->Particle->Respawn(); } } void SparkParticleEditor::DiscardChanges() { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_impl->Particle.has_value(), "SPARK particle editor has no particle to discard", _assetPath); _changed = _impl->Changed = false; GetSparkParticleRuntimeSystem(&_impl->Particle.value())->ReplaceBaseSystem(SPK::System::copy(_impl->SystemBackup)); _saveError.clear(); } auto SparkParticleEditor::SaveChanges() -> bool { FO_STACK_TRACE_ENTRY(); if (strex(_assetPath).get_file_extension() != "spark") { _saveError = "Save is unavailable for this source format"; return false; } auto file = _impl->RawResources->ReadFileHeader(_assetPath); FO_VERIFY_AND_THROW(file, "SPARK particle editor could not resolve raw particle asset for saving", _assetPath); FO_VERIFY_AND_THROW(_impl->Particle.has_value(), "SPARK particle editor has no particle to save", _assetPath); SPK::Ref<SPK::System> base_system = GetSparkParticleRuntimeSystem(&_impl->Particle.value())->GetEditableBaseSystem(); nptr<const SPK::IO::Saver> saver = base_system->getContext().getIOManager().getSaver("xml"); FO_VERIFY_AND_THROW(saver, "Missing required saver"); std::string path {file.GetDiskPath()}; if (!saver->save(path, base_system.get(), path)) { _saveError = strex("Failed to save particle source '{}'", _assetPath); WriteLog(LogType::Error, "SPARK particle editor failed to save '{}'", _assetPath); return false; } _changed = _impl->Changed = false; _impl->SystemBackup = SPK::System::copy(SPK::Ref<SPK::System>(base_system.get())); _impl->Particle->Respawn(); _saveError.clear(); _onSaved(_assetPath); return true; } #define DRAW_SPK_FLOAT(label, get, set) \ do { \ float32_t val = obj->get(); \ Changed |= ImGui::InputFloat(label, &val); \ obj->set(val); \ } while (0) #define DRAW_SPK_BOOL(label, get, set) \ do { \ bool val = obj->get(); \ Changed |= ImGui::Checkbox(label, &val); \ obj->set(val); \ } while (0) #define DRAW_SPK_BOOL_BOOL(label1, label2, get1, get2, set) \ do { \ bool val1 = obj->get1(); \ bool val2 = obj->get2(); \ Changed |= ImGui::Checkbox(label1, &val1); \ Changed |= ImGui::Checkbox(label2, &val2); \ obj->set(val1, val2); \ } while (0) #define DRAW_SPK_FLOAT_FLOAT(label1, label2, get1, get2, set) \ do { \ float32_t val1 = obj->get1(); \ float32_t val2 = obj->get2(); \ Changed |= ImGui::InputFloat(label1, &val1); \ Changed |= ImGui::InputFloat(label2, &val2); \ obj->set(val1, val2); \ } while (0) #define DRAW_SPK_INT(label, get, set) \ do { \ int32_t val = numeric_cast<int32_t>(obj->get()); \ Changed |= ImGui::InputInt(label, &val); \ obj->set(val); \ } while (0) #define DRAW_SPK_INT_INT(label1, label2, get1, get2, set) \ do { \ int32_t val1 = numeric_cast<int32_t>(obj->get1()); \ int32_t val2 = numeric_cast<int32_t>(obj->get2()); \ Changed |= ImGui::InputInt(label1, &val1); \ Changed |= ImGui::InputInt(label2, &val2); \ obj->set(val1, val2); \ } while (0) #define DRAW_SPK_FLOAT_BOOL(label1, label2, get1, get2, set) \ do { \ float32_t val1 = obj->get1(); \ bool val2 = obj->get2(); \ Changed |= ImGui::InputFloat(label1, &val1); \ Changed |= ImGui::Checkbox(label2, &val2); \ obj->set(val1, val2); \ } while (0) #define DRAW_SPK_VECTOR(label, get, set) \ do { \ float32_t val[3] = {obj->get().x, obj->get().y, obj->get().z}; \ Changed |= ImGui::InputFloat3(label, val); \ obj->set(SPK::Vector3D(val[0], val[1], val[2])); \ } while (0) #define DRAW_SPK_VECTOR_VECTOR(label1, label2, get1, get2, set) \ do { \ float32_t val1[3] = {obj->get1().x, obj->get1().y, obj->get1().z}; \ float32_t val2[3] = {obj->get2().x, obj->get2().y, obj->get2().z}; \ Changed |= ImGui::InputFloat3(label1, val1); \ Changed |= ImGui::InputFloat3(label2, val2); \ obj->set(SPK::Vector3D(val1[0], val1[1], val1[2]), SPK::Vector3D(val2[0], val2[1], val2[2])); \ } while (0) #define DRAW_SPK_COLOR(label, get, set) \ do { \ int32_t val[4] = {obj->get().r, obj->get().g, obj->get().b, obj->get().a}; \ Changed |= ImGui::InputInt4(label, val); \ obj->set(SPK::Color(val[0], val[1], val[2], val[3])); \ } while (0) #define DRAW_SPK_COLOR_COLOR(label1, label2, get1, get2, set) \ do { \ int32_t val1[4] = {obj->get1().r, obj->get1().g, obj->get1().b, obj->get1().a}; \ int32_t val2[4] = {obj->get2().r, obj->get2().g, obj->get2().b, obj->get2().a}; \ Changed |= ImGui::InputInt4(label1, val1); \ Changed |= ImGui::InputInt4(label2, val2); \ obj->set(SPK::Color(val1[0], val1[1], val1[2], val1[3]), SPK::Color(val2[0], val2[1], val2[2], val2[3])); \ } while (0) #define DRAW_SPK_COMBO(label, get, set, ...) \ do { \ int32_t val = static_cast<int32_t>(obj->get()); \ const char* items[] = {__VA_ARGS__}; \ Changed |= ImGui::Combo(label, &val, items, sizeof(items) / sizeof(items[0])); \ obj->set(static_cast<decltype(obj->get())>(val)); \ } while (0) #define DRAW_SPK_COMBO_COMBO(label1, label2, get1, get2, set) \ do { \ int32_t val1 = static_cast<int32_t>(obj->get1()); \ int32_t val2 = static_cast<int32_t>(obj->get2()); \ Changed |= ImGui::Combo(label1, &val1, items1, sizeof(items1) / sizeof(items1[0])); \ Changed |= ImGui::Combo(label2, &val2, items2, sizeof(items2) / sizeof(items2[0])); \ obj->set(static_cast<decltype(obj->get1())>(val1), static_cast<decltype(obj->get2())>(val2)); \ } while (0) #define DRAW_SPK_COMBO_COMBO_COMBO(label1, label2, label3, get1, get2, get3, set) \ do { \ auto val1 = static_cast<int32_t>(obj->get1()); \ auto val2 = static_cast<int32_t>(obj->get2()); \ auto val3 = static_cast<int32_t>(obj->get3()); \ Changed |= ImGui::Combo(label1, &val1, items1, sizeof(items1) / sizeof(items1[0])); \ Changed |= ImGui::Combo(label2, &val2, items2, sizeof(items2) / sizeof(items2[0])); \ Changed |= ImGui::Combo(label3, &val3, items3, sizeof(items3) / sizeof(items3[0])); \ obj->set(static_cast<decltype(obj->get1())>(val1), static_cast<decltype(obj->get2())>(val2), static_cast<decltype(obj->get3())>(val3)); \ } while (0) #define DRAW_SPK_INTERPOLATOR_FIELD(name, get, set) \ DrawSparkNullableField( \ name, [obj] { return obj->get(); }, [obj] { obj->set(SPK::Ref<SPK::FloatInterpolator>()); }, \ [obj] { \ if (ImGui::Button("Add FloatDefaultInitializer")) { \ obj->set(SPK::FloatDefaultInitializer::create(0.0f)); \ } \ if (ImGui::Button("Add FloatRandomInitializer")) { \ obj->set(SPK::FloatRandomInitializer::create(0.0f, 0.0f)); \ } \ if (ImGui::Button("Add FloatSimpleInterpolator")) { \ obj->set(SPK::FloatSimpleInterpolator::create(0.0f, 0.0f)); \ } \ if (ImGui::Button("Add FloatRandomInterpolator")) { \ obj->set(SPK::FloatRandomInterpolator::create(0.0f, 0.0f, 0.0f, 0.0f)); \ } \ if (ImGui::Button("Add FloatGraphInterpolator")) { \ obj->set(SPK::FloatGraphInterpolator::create()); \ } \ }) // Generic void SparkParticleEditor::Impl::DrawGenericSparkObject(const SPK::Ref<SPK::SPKObject>& obj) { FO_STACK_TRACE_ENTRY(); if (NamingMode) { char buf[1000]; strcpy(buf, obj->getName().c_str()); if (ImGui::InputText("Name", buf, 1000)) { Changed |= true; obj->setName(buf); } } /*auto shared = obj->isShared(); if (ImGui::Checkbox("Shared", &shared)) { Changed |= true; obj->setShared(shared); }*/ #define CHECK_AND_DRAW(cls) \ do { \ if (auto&& p = SPK::dynamicCast<cls>(obj)) { \ DrawSparkObject(p); \ return; \ } \ } while (0) CHECK_AND_DRAW(SPK::System); CHECK_AND_DRAW(SPK::Group); CHECK_AND_DRAW(SPK::FloatDefaultInitializer); CHECK_AND_DRAW(SPK::ColorDefaultInitializer); CHECK_AND_DRAW(SPK::FloatRandomInitializer); CHECK_AND_DRAW(SPK::ColorRandomInitializer); CHECK_AND_DRAW(SPK::FloatSimpleInterpolator); CHECK_AND_DRAW(SPK::ColorSimpleInterpolator); CHECK_AND_DRAW(SPK::FloatRandomInterpolator); CHECK_AND_DRAW(SPK::ColorRandomInterpolator); CHECK_AND_DRAW(SPK::FloatGraphInterpolator); CHECK_AND_DRAW(SPK::ColorGraphInterpolator); CHECK_AND_DRAW(SPK::Point); CHECK_AND_DRAW(SPK::Sphere); CHECK_AND_DRAW(SPK::Plane); CHECK_AND_DRAW(SPK::Ring); CHECK_AND_DRAW(SPK::Box); CHECK_AND_DRAW(SPK::Cylinder); CHECK_AND_DRAW(SPK::StaticEmitter); CHECK_AND_DRAW(SPK::RandomEmitter); CHECK_AND_DRAW(SPK::StraightEmitter); CHECK_AND_DRAW(SPK::SphericEmitter); CHECK_AND_DRAW(SPK::NormalEmitter); CHECK_AND_DRAW(SPK::Gravity); CHECK_AND_DRAW(SPK::Friction); CHECK_AND_DRAW(SPK::Obstacle); CHECK_AND_DRAW(SPK::Rotator); CHECK_AND_DRAW(SPK::Collider); CHECK_AND_DRAW(SPK::Destroyer); CHECK_AND_DRAW(SPK::Vortex); CHECK_AND_DRAW(SPK::EmitterAttacher); CHECK_AND_DRAW(SPK::PointMass); CHECK_AND_DRAW(SPK::RandomForce); CHECK_AND_DRAW(SPK::LinearForce); CHECK_AND_DRAW(SPK::ActionSet); CHECK_AND_DRAW(SPK::SpawnParticlesAction); #undef CHECK_AND_DRAW SPK::Ref<SPK::Renderer> renderer = SPK::dynamicCast<SPK::Renderer>(obj); if (renderer && SPK::FO::IsSparkQuadRenderer(*renderer)) { DrawSparkObject(renderer); return; } FO_UNREACHABLE_PLACE(); } // Core void SparkParticleEditor::Impl::DrawSparkTransformable(const SPK::Ref<SPK::Transformable>& obj) { FO_STACK_TRACE_ENTRY(); bool is_custom_transform = !obj->getTransform().isLocalIdentity(); if (is_custom_transform) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.8f, 0.0f, 1.0f)); } if (ImGui::TreeNodeEx("Transform")) { if (is_custom_transform) { ImGui::PopStyleColor(); } Changed |= ImGui::InputFloat3("Position", const_cast<float32_t*>(std::addressof(obj->getTransform().getLocal()[12]))); Changed |= ImGui::InputFloat3("UpAxis", const_cast<float32_t*>(std::addressof(obj->getTransform().getLocal()[4]))); if (ImGui::Button("Fix transform")) { Changed |= true; obj->getTransform().setPosition(obj->getTransform().getLocalPos()); } if (is_custom_transform) { if (ImGui::Button("Reset transform")) { Changed |= true; obj->getTransform().reset(); } } ImGui::TreePop(); } else { if (is_custom_transform) { ImGui::PopStyleColor(); } } } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::System>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkTransformable(obj); DrawSparkArray( "Groups", true, [obj] { return obj->getNbGroups(); }, [obj](auto i) { return obj->getGroup(i); }, [obj](auto i) { obj->removeGroup(obj->getGroup(i)); }, [obj] { if (ImGui::Button("Add group")) { obj->addGroup(SPK::Group::create()); } else if (ImGui::Button("Clone group")) { obj->addGroup(SPK::Group::copy(obj->getGroup(obj->getNbGroups() - 1))); } }); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Group>& obj) { FO_STACK_TRACE_ENTRY(); if (auto&& renderer = obj->getRenderer()) { bool val = renderer->isActive(); Changed |= ImGui::Checkbox("Active", &val); renderer->setActive(val); } DrawSparkTransformable(obj); DRAW_SPK_INT("Capacity", getCapacity, reallocate); DRAW_SPK_FLOAT_FLOAT("Min life time", "Max life time", getMinLifeTime, getMaxLifeTime, setLifeTime); DRAW_SPK_BOOL("Immortal", isImmortal, setImmortal); DRAW_SPK_BOOL("Still", isStill, setStill); DRAW_SPK_BOOL("Distance computation", isDistanceComputationEnabled, enableDistanceComputation); DRAW_SPK_BOOL("Sorting", isSortingEnabled, enableSorting); DRAW_SPK_FLOAT("Radius", getGraphicalRadius, setRadius); DrawSparkNullableField( "Color interpolator", [obj] { return obj->getColorInterpolator(); }, [obj] { obj->setColorInterpolator(SPK::Ref<SPK::ColorInterpolator>()); }, [obj] { if (ImGui::Button("Add ColorDefaultInitializer")) { obj->setColorInterpolator(SPK::ColorDefaultInitializer::create(SPK::Color())); } if (ImGui::Button("Add ColorRandomInitializer")) { obj->setColorInterpolator(SPK::ColorRandomInitializer::create(SPK::Color(), SPK::Color())); } if (ImGui::Button("Add ColorSimpleInterpolator")) { obj->setColorInterpolator(SPK::ColorSimpleInterpolator::create(SPK::Color(), SPK::Color())); } if (ImGui::Button("Add ColorRandomInterpolator")) { obj->setColorInterpolator(SPK::ColorRandomInterpolator::create(SPK::Color(), SPK::Color(), SPK::Color(), SPK::Color())); } if (ImGui::Button("Add ColorGraphInterpolator")) { obj->setColorInterpolator(SPK::ColorGraphInterpolator::create()); } }); DRAW_SPK_INTERPOLATOR_FIELD("Scale interpolator", getScaleInterpolator, setScaleInterpolator); DRAW_SPK_INTERPOLATOR_FIELD("Mass interpolator", getMassInterpolator, setMassInterpolator); DRAW_SPK_INTERPOLATOR_FIELD("Angle interpolator", getAngleInterpolator, setAngleInterpolator); DRAW_SPK_INTERPOLATOR_FIELD("Texture index interpolator", getTextureIndexInterpolator, setTextureIndexInterpolator); DRAW_SPK_INTERPOLATOR_FIELD("Rotation speed interpolator", getRotationSpeedInterpolator, setRotationSpeedInterpolator); DrawSparkArray( "Emitters", false, [obj] { return obj->getNbEmitters(); }, [obj](auto i) { return obj->getEmitter(i); }, [obj](auto i) { obj->removeEmitter(obj->getEmitter(i)); }, [obj] { if (ImGui::Button("Add StaticEmitter")) { obj->addEmitter(SPK::StaticEmitter::create()); } if (ImGui::Button("Add RandomEmitter")) { obj->addEmitter(SPK::RandomEmitter::create()); } if (ImGui::Button("Add StraightEmitter")) { obj->addEmitter(SPK::StraightEmitter::create()); } if (ImGui::Button("Add SphericEmitter")) { obj->addEmitter(SPK::SphericEmitter::create()); } if (ImGui::Button("Add NormalEmitter")) { obj->addEmitter(SPK::NormalEmitter::create()); } }); DrawSparkArray( "Modifiers", false, [obj] { return obj->getNbModifiers(); }, [obj](auto i) { return obj->getModifier(i); }, [obj](auto i) { obj->removeModifier(obj->getModifier(i)); }, [this, obj] { if (ImGui::Button("Add Gravity")) { obj->addModifier(SPK::Gravity::create()); } if (ImGui::Button("Add Friction")) { obj->addModifier(SPK::Friction::create()); } if (ImGui::Button("Add Obstacle")) { obj->addModifier(SPK::Obstacle::create()); } if (ImGui::Button("Add Rotator")) { obj->addModifier(SPK::Rotator::create()); } if (ImGui::Button("Add Collider")) { obj->addModifier(SPK::Collider::create()); } if (ImGui::Button("Add Destroyer")) { obj->addModifier(SPK::Destroyer::create()); } if (ImGui::Button("Add Vortex")) { obj->addModifier(SPK::Vortex::create()); } if (ImGui::Button("Add EmitterAttacher")) { obj->addModifier(SPK::EmitterAttacher::create(obj, GetFirstSparkEmitter(obj))); } if (ImGui::Button("Add PointMass")) { obj->addModifier(SPK::PointMass::create()); } if (ImGui::Button("Add RandomForce")) { obj->addModifier(SPK::RandomForce::create()); } if (ImGui::Button("Add LinearForce")) { obj->addModifier(SPK::LinearForce::create()); } }); DrawSparkNullableField( "Birth action", [obj] { return obj->getBirthAction(); }, [obj] { obj->setBirthAction(SPK::Ref<SPK::Action>()); }, [this, obj] { // DrawSparkActionAddButtons([obj](const SPK::Ref<SPK::Action>& action) { obj->setBirthAction(action); }, obj); }); DrawSparkNullableField( "Death action", [obj] { return obj->getDeathAction(); }, [obj] { obj->setDeathAction(SPK::Ref<SPK::Action>()); }, [this, obj] { // DrawSparkActionAddButtons([obj](const SPK::Ref<SPK::Action>& action) { obj->setDeathAction(action); }, obj); }); DrawSparkNullableField( "Renderer", [obj] { return obj->getRenderer(); }, [obj] { obj->setRenderer(SPK::Ref<SPK::Renderer>()); }, [obj] { if (ImGui::Button("Add SparkQuadRenderer")) { obj->setRenderer(SPK::FO::CreateSparkQuadRenderer()); } }); } // Interpolators void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::FloatDefaultInitializer>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_FLOAT("Default value", getDefaultValue, setDefaultValue); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ColorDefaultInitializer>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_COLOR("Default color", getDefaultValue, setDefaultValue); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::FloatRandomInitializer>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_FLOAT_FLOAT("Min value", "Max value", getMinValue, getMaxValue, setValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ColorRandomInitializer>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_COLOR_COLOR("Min color", "Max color", getMinValue, getMaxValue, setValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::FloatSimpleInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_FLOAT_FLOAT("Birth value", "Death value", getBirthValue, getDeathValue, setValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ColorSimpleInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_COLOR_COLOR("Birth value", "Death value", getBirthValue, getDeathValue, setValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::FloatRandomInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_FLOAT_FLOAT("Birth min value", "Birth max value", getMinBirthValue, getMaxBirthValue, setBirthValues); DRAW_SPK_FLOAT_FLOAT("Death min value", "Death max value", getMinDeathValue, getMaxDeathValue, setDeathValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ColorRandomInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_COLOR_COLOR("Birth min color", "Birth max color", getMinBirthValue, getMaxBirthValue, setBirthValues); DRAW_SPK_COLOR_COLOR("Death min color", "Death max color", getMinDeathValue, getMaxDeathValue, setDeathValues); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::FloatGraphInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); auto&& graph = obj->getGraph(); if (ImGui::TreeNodeEx("Keys", 0, "Keys (%d)", numeric_cast<int32_t>(graph.size()))) { int32_t delIndex = -1; int32_t index = 0; for (auto it = graph.begin(); it != graph.end(); ++it) { using Entry = std::remove_const_t<std::remove_reference_t<decltype(*it)>>; ptr<Entry> entry = const_cast<Entry*>(std::addressof(*it)); string name = strex("{}: {} => {}", entry->x, entry->y0, entry->y1); if (ImGui::TreeNodeEx(entry.get(), ImGuiTreeNodeFlags_DefaultOpen, "%s", name.c_str())) { ImGui::InputFloat("Start", &entry->y0); ImGui::InputFloat("End", &entry->y1); ImGui::TreePop(); } if (RemovingMode) { if (ImGui::Button(strex("Remove at {}", entry->x).c_str())) { delIndex = index; } } index++; } if (delIndex != -1) { auto it = graph.begin(); for (int32_t i = 0; i < delIndex; i++) { ++it; } graph.erase(it); } if (AddingMode) { ImGui::InputFloat("Position", &FloatGraphEntryPosition); if (ImGui::Button("Insert entry")) { obj->addEntry(FloatGraphEntryPosition, 0.0f, 0.0f); } } ImGui::TreePop(); } const char* items1[] = {"INTERPOLATOR_LIFETIME", "INTERPOLATOR_AGE", "INTERPOLATOR_PARAM", "INTERPOLATOR_VELOCITY"}; const char* items2[] = {"PARAM_SCALE", "PARAM_MASS", "PARAM_ANGLE", "PARAM_TEXTURE_INDEX", "PARAM_ROTATION_SPEED"}; DRAW_SPK_COMBO_COMBO("Interpolation type", "Interpolation param", getType, getInterpolatorParam, setType); DRAW_SPK_BOOL("Looping", isLoopingEnabled, enableLooping); DRAW_SPK_FLOAT("Scale variation", getScaleXVariation, setScaleXVariation); DRAW_SPK_FLOAT("Offset variation", getOffsetXVariation, setOffsetXVariation); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ColorGraphInterpolator>& obj) { FO_STACK_TRACE_ENTRY(); auto&& graph = obj->getGraph(); if (ImGui::TreeNodeEx("Keys", 0, "Keys (%d)", numeric_cast<int32_t>(graph.size()))) { int32_t delIndex = -1; int32_t index = 0; for (auto it = graph.begin(); it != graph.end(); ++it) { using Entry = std::remove_const_t<std::remove_reference_t<decltype(*it)>>; ptr<Entry> entry = const_cast<Entry*>(std::addressof(*it)); string name = strex("{}: ({}, {}, {}, {}) => ({}, {}, {}, {})", entry->x, entry->y0.r, entry->y0.g, entry->y0.b, entry->y0.a, entry->y1.r, entry->y1.g, entry->y1.b, entry->y1.a); if (ImGui::TreeNodeEx(entry.get(), ImGuiTreeNodeFlags_DefaultOpen, "%s", name.c_str())) { int32_t c1[] = {entry->y0.r, entry->y0.g, entry->y0.b, entry->y0.a}; ImGui::InputInt4("Start", c1); entry->y0.r = numeric_cast<unsigned char>(c1[0]); entry->y0.g = numeric_cast<unsigned char>(c1[1]); entry->y0.b = numeric_cast<unsigned char>(c1[2]); entry->y0.a = numeric_cast<unsigned char>(c1[3]); int32_t c2[] = {entry->y1.r, entry->y1.g, entry->y1.b, entry->y1.a}; ImGui::InputInt4("End", c2); entry->y1.r = numeric_cast<unsigned char>(c2[0]); entry->y1.g = numeric_cast<unsigned char>(c2[1]); entry->y1.b = numeric_cast<unsigned char>(c2[2]); entry->y1.a = numeric_cast<unsigned char>(c2[3]); ImGui::TreePop(); } if (RemovingMode) { if (ImGui::Button(strex("Remove at {}", entry->x).c_str())) { delIndex = index; } } index++; } if (delIndex != -1) { auto it = graph.begin(); for (int32_t i = 0; i < delIndex; i++) { ++it; } graph.erase(it); } if (AddingMode) { ImGui::InputFloat("Position", &ColorGraphEntryPosition); if (ImGui::Button("Insert entry")) { obj->addEntry(ColorGraphEntryPosition, SPK::Color(), SPK::Color()); } } ImGui::TreePop(); } const char* items1[] = {"INTERPOLATOR_LIFETIME", "INTERPOLATOR_AGE", "INTERPOLATOR_PARAM", "INTERPOLATOR_VELOCITY"}; const char* items2[] = {"PARAM_SCALE", "PARAM_MASS", "PARAM_ANGLE", "PARAM_TEXTURE_INDEX", "PARAM_ROTATION_SPEED"}; DRAW_SPK_COMBO_COMBO("Interpolation type", "Interpolation param", getType, getInterpolatorParam, setType); DRAW_SPK_BOOL("Looping", isLoopingEnabled, enableLooping); DRAW_SPK_FLOAT("Scale variation", getScaleXVariation, setScaleXVariation); DRAW_SPK_FLOAT("Offset variation", getOffsetXVariation, setOffsetXVariation); } // Zones void SparkParticleEditor::Impl::DrawSparkInnerZone(const char* name, const function<SPK::Ref<SPK::Zone>()>& get, const function<void(const SPK::Ref<SPK::Zone>&)>& set) { FO_STACK_TRACE_ENTRY(); if (ImGui::TreeNodeEx(name)) { if (get()) { DrawGenericSparkObject(get()); if (RemovingMode) { if (ImGui::Button("Remove zone")) { Changed |= true; set(SPK::Ref<SPK::Zone>()); } } } else { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); ImGui::Text("%s", "No zone"); ImGui::PopStyleColor(); if (AddingMode) { if (ImGui::Button("Add Point normal zone")) { set(SPK::Point::create()); } if (ImGui::Button("Add Sphere normal zone")) { set(SPK::Sphere::create()); } if (ImGui::Button("Add Plane normal zone")) { set(SPK::Plane::create()); } if (ImGui::Button("Add Ring normal zone")) { set(SPK::Ring::create()); } if (ImGui::Button("Add Box normal zone")) { set(SPK::Box::create()); } if (ImGui::Button("Add Cylinder normal zone")) { set(SPK::Cylinder::create()); } Changed |= !!get(); } } ImGui::TreePop(); } } void SparkParticleEditor::Impl::DrawSparkZone(const SPK::Ref<SPK::Zone>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkTransformable(obj); DRAW_SPK_VECTOR("Position", getPosition, setPosition); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Point>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Sphere>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); DRAW_SPK_FLOAT("Sphere Radius", getRadius, setRadius); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Plane>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); DRAW_SPK_VECTOR("Plane Normal", getNormal, setNormal); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Ring>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); DRAW_SPK_VECTOR("Ring Normal", getNormal, setNormal); DRAW_SPK_FLOAT_FLOAT("Ring MinRadius", "Ring MaxRadius", getMinRadius, getMaxRadius, setRadius); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Box>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); DRAW_SPK_VECTOR("Box Dimension", getDimensions, setDimensions); DRAW_SPK_VECTOR_VECTOR("Box FrontAxis", "Box UpAxis", getZAxis, getYAxis, setAxis); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Cylinder>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZone(obj); DRAW_SPK_FLOAT_FLOAT("Cylinder Height", "Cylinder Radius", getHeight, getRadius, setDimensions); DRAW_SPK_VECTOR("Cylinder Axis", getAxis, setAxis); } // Emitters void SparkParticleEditor::Impl::DrawSparkEmitter(const SPK::Ref<SPK::Emitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkTransformable(obj); DRAW_SPK_BOOL("Active", isActive, setActive); int32_t min_tank = obj->getMinTank(); bool min_changed = Changed |= ImGui::InputInt("MinTank", &min_tank); int32_t max_tank = obj->getMaxTank(); Changed |= ImGui::InputInt("MaxTank", &max_tank); if ((min_tank >= 0) != (max_tank >= 0)) { if (min_changed) { max_tank = min_tank; } else { min_tank = max_tank; } } obj->setTank(min_tank, max_tank); DRAW_SPK_FLOAT("Flow", getFlow, setFlow); DRAW_SPK_FLOAT_FLOAT("MinForce", "MaxForce", getForceMin, getForceMax, setForce); DRAW_SPK_BOOL("UseFullZone", isFullZone, setUseFullZone); DrawSparkInnerZone("Zone", [obj] { return obj->getZone() != obj->getContext().getDefaultZone() ? obj->getZone() : SPK::Ref<SPK::Zone>(); }, [obj](auto&& zone) { obj->setZone(zone); }); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::StaticEmitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkEmitter(obj); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::RandomEmitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkEmitter(obj); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::StraightEmitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkEmitter(obj); DRAW_SPK_VECTOR("StraightEmitter Direction", getDirection, setDirection); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::SphericEmitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkEmitter(obj); DRAW_SPK_VECTOR("SphericEmitter Direction", getDirection, setDirection); DRAW_SPK_FLOAT_FLOAT("SphericEmitter MinAngle", "SphericEmitter MaxAngle", getAngleMin, getAngleMax, setAngles); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::NormalEmitter>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkEmitter(obj); DrawSparkInnerZone("NormalEmitter NormalZone", [obj] { return obj->getNormalZone(); }, [obj](auto&& zone) { obj->setNormalZone(zone); }); DRAW_SPK_BOOL("NormalEmitter InvertedNormals", isInverted, setInverted); } // Modifiers void SparkParticleEditor::Impl::DrawSparkModifier(const SPK::Ref<SPK::Modifier>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkTransformable(obj); DRAW_SPK_BOOL("Active", isActive, setActive); DRAW_SPK_BOOL("LocalToSystem", isLocalToSystem, setLocalToSystem); } void SparkParticleEditor::Impl::DrawSparkZonedModifier(const SPK::Ref<SPK::ZonedModifier>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DrawSparkInnerZone("Zone", [obj] { return obj->getZone(); }, [obj](auto&& zone) { obj->setZone(zone); }); DRAW_SPK_COMBO("ZoneTest", getZoneTest, setZoneTest, "ZONE_TEST_INSIDE", "ZONE_TEST_OUTSIDE", "ZONE_TEST_INTERSECT", "ZONE_TEST_ENTER", "ZONE_TEST_LEAVE", "ZONE_TEST_ALWAYS"); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Gravity>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DRAW_SPK_VECTOR("Gravity Value", getValue, setValue); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Friction>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); float32_t value = obj->value; Changed |= ImGui::InputFloat("Friction Value", &value); obj->value = value; } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Obstacle>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZonedModifier(obj); DRAW_SPK_FLOAT("Obstacle BouncingRatio", getBouncingRatio, setBouncingRatio); DRAW_SPK_FLOAT("Obstacle Friction", getFriction, setFriction); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Rotator>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Collider>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DRAW_SPK_FLOAT("Collider Elasticity", getElasticity, setElasticity); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Destroyer>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZonedModifier(obj); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Vortex>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DRAW_SPK_VECTOR("Vortex Position", getPosition, setPosition); DRAW_SPK_VECTOR("Vortex Direction", getDirection, setDirection); DRAW_SPK_FLOAT_BOOL("Vortex RotationSpeed", "Vortex RotationSpeedAngular", getRotationSpeed, isRotationSpeedAngular, setRotationSpeed); DRAW_SPK_FLOAT_BOOL("Vortex AttractionSpeed", "Vortex AttractionSpeedLinear", getAttractionSpeed, isAttractionSpeedLinear, setAttractionSpeed); DRAW_SPK_FLOAT("Vortex EyeRadius", getEyeRadius, setEyeRadius); DRAW_SPK_BOOL("Vortex KillingParticles", isParticleKillingEnabled, enableParticleKilling); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::EmitterAttacher>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DrawSparkEmitterRef("EmitterAttacher BaseEmitter", obj->getEmitter(), [obj](const SPK::Ref<SPK::Emitter>& emitter) { obj->setEmitter(emitter); }); DRAW_SPK_BOOL_BOOL("EmitterAttacher OrientationEnabled", "EmitterAttacher RotationEnabled", isEmitterOrientationEnabled, isEmitterRotationEnabled, enableEmitterOrientation); DrawSparkGroupRef("EmitterAttacher TargetGroup", obj->getTargetGroup(), [obj](const SPK::Ref<SPK::Group>& group) { obj->setTargetGroup(group); }); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::PointMass>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DRAW_SPK_VECTOR("PointMass Position", getPosition, setPosition); DRAW_SPK_FLOAT("PointMass Mass", getMass, setMass); DRAW_SPK_FLOAT("PointMass Offset", getOffset, setOffset); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::RandomForce>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkModifier(obj); DRAW_SPK_VECTOR_VECTOR("RandomForce MinVector", "RandomForce MaxVector", getMinVector, getMaxVector, setVectors); DRAW_SPK_FLOAT_FLOAT("RandomForce MinPeriod", "RandomForce MaxPeriod", getMinPeriod, getMaxPeriod, setPeriods); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::LinearForce>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkZonedModifier(obj); DRAW_SPK_VECTOR("LinearForce Value", getValue, setValue); DRAW_SPK_BOOL_BOOL("LinearForce Relative", "LinearForce RelativeSquaredSpeedUsed", isRelative, isSquaredSpeedUsed, setRelative); const char* items1[] = {"PARAM_SCALE", "PARAM_MASS", "PARAM_ANGLE", "PARAM_TEXTURE_INDEX", "PARAM_ROTATION_SPEED"}; const char* items2[] = {"FACTOR_CONSTANT", "FACTOR_LINEAR", "FACTOR_QUADRATIC", "FACTOR_CUBIC"}; DRAW_SPK_COMBO_COMBO("LinearForce Param", "LinearForce Factor", getParam, getFactor, setParam); DRAW_SPK_FLOAT("LinearForce Coef", getCoef, setCoef); } // Actions void SparkParticleEditor::Impl::DrawSparkAction(const SPK::Ref<SPK::Action>& obj) { FO_STACK_TRACE_ENTRY(); DRAW_SPK_BOOL("Active", isActive, setActive); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::ActionSet>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkAction(obj); DrawSparkArray( "Actions", false, [obj] { return obj->getNbActions(); }, [obj](auto i) { return obj->getAction(i); }, [obj](auto i) { obj->removeAction(obj->getAction(i)); }, [this, obj] { // DrawSparkActionAddButtons([obj](const SPK::Ref<SPK::Action>& action) { obj->addAction(action); }, SPK::Ref<SPK::Group>()); }); } void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::SpawnParticlesAction>& obj) { FO_STACK_TRACE_ENTRY(); DrawSparkAction(obj); int32_t min_nb = numeric_cast<int32_t>(obj->getMinNb()); int32_t max_nb = numeric_cast<int32_t>(obj->getMaxNb()); bool nb_changed = ImGui::InputInt("SpawnParticlesAction MinNb", &min_nb); nb_changed |= ImGui::InputInt("SpawnParticlesAction MaxNb", &max_nb); if (nb_changed) { min_nb = std::max(min_nb, 0); max_nb = std::max(max_nb, 0); Changed |= true; obj->setNb(numeric_cast<unsigned int>(min_nb), numeric_cast<unsigned int>(max_nb)); } DrawSparkEmitterRef("SpawnParticlesAction BaseEmitter", obj->getEmitter(), [obj](const SPK::Ref<SPK::Emitter>& emitter) { obj->setEmitter(emitter); }); DrawSparkGroupRef("SpawnParticlesAction TargetGroup", obj->getTargetGroup(), [obj](const SPK::Ref<SPK::Group>& group) { obj->setTargetGroup(group); }); if (ImGui::Button("SpawnParticlesAction ResetPool")) { Changed |= true; obj->resetPool(); } } // Renderers void SparkParticleEditor::Impl::DrawSparkObject(const SPK::Ref<SPK::Renderer>& obj) { FO_STACK_TRACE_ENTRY(); SPK::FO::SparkQuadRendererData data = SPK::FO::GetSparkQuadRendererData(*obj); bool renderer_changed = false; renderer_changed |= ImGui::Checkbox("Active", &data.Active); renderer_changed |= ImGui::Checkbox("AlphaTest", &data.AlphaTest); renderer_changed |= ImGui::Checkbox("DepthWrite", &data.DepthWrite); renderer_changed |= ImGui::InputFloat("AlphaThreshold", &data.AlphaTestThreshold); renderer_changed |= ImGui::Checkbox("DrawInScene", &data.DrawInScene); // Effect { int32_t index = -1; if (auto it = std::ranges::find(AllEffects, data.EffectName); it != AllEffects.end()) { index = numeric_cast<int32_t>(std::distance(AllEffects.begin(), it)); } string preview = index >= 0 ? AllEffects[numeric_cast<size_t>(index)] : data.EffectName; if (ImGui::BeginCombo("Effect", preview.c_str())) { for (size_t i = 0; i < AllEffects.size(); i++) { bool selected = index == numeric_cast<int32_t>(i); if (ImGui::Selectable(AllEffects[i].c_str(), selected)) { renderer_changed = true; data.EffectName = AllEffects[i]; } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } } // Texture { int32_t index = -1; if (auto it = std::ranges::find(AllTextures, data.TextureName); it != AllTextures.end()) { index = numeric_cast<int32_t>(std::distance(AllTextures.begin(), it)); } string preview = index >= 0 ? AllTextures[numeric_cast<size_t>(index)] : data.TextureName; if (ImGui::BeginCombo("Texture", preview.c_str())) { for (size_t i = 0; i < AllTextures.size(); i++) { bool selected = index == numeric_cast<int32_t>(i); if (ImGui::Selectable(AllTextures[i].c_str(), selected)) { renderer_changed = true; data.TextureName = AllTextures[i]; } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } } renderer_changed |= ImGui::InputFloat("ScaleX", &data.ScaleX); renderer_changed |= ImGui::InputFloat("ScaleY", &data.ScaleY); renderer_changed |= ImGui::InputInt("AtlasDimensionX", &data.AtlasDimensionX); renderer_changed |= ImGui::InputInt("AtlasDimensionY", &data.AtlasDimensionY); const char* look_orientation_items[] = {"LOOK_CAMERA_PLANE", "LOOK_CAMERA_POINT", "LOOK_AXIS", "LOOK_POINT"}; const char* up_orientation_items[] = {"UP_CAMERA", "UP_DIRECTION", "UP_AXIS", "UP_POINT"}; const char* locked_axis_items[] = {"LOCK_LOOK", "LOCK_UP"}; renderer_changed |= ImGui::Combo("LookOrientation", &data.LookOrientation, look_orientation_items, std::size(look_orientation_items)); renderer_changed |= ImGui::Combo("UpOrientation", &data.UpOrientation, up_orientation_items, std::size(up_orientation_items)); renderer_changed |= ImGui::Combo("LockedAxis", &data.LockedAxis, locked_axis_items, std::size(locked_axis_items)); renderer_changed |= ImGui::InputFloat3("LookVector", data.LookVector.data()); renderer_changed |= ImGui::InputFloat3("UpVector", data.UpVector.data()); if (renderer_changed) { SPK::FO::SetSparkQuadRendererData(*obj, data); Changed = true; } } // Helpers void SparkParticleEditor::Impl::DrawSparkGroupRef(const char* label, const SPK::Ref<SPK::Group>& current, const function<void(const SPK::Ref<SPK::Group>&)>& set) { FO_STACK_TRACE_ENTRY(); vector<SPK::Ref<SPK::Group>> refs {SPK::Ref<SPK::Group>()}; vector<string> names {"None"}; auto groups = GetSparkGroups(); for (size_t i = 0; i < groups.size(); i++) { refs.emplace_back(groups[i]); names.emplace_back(GetSparkObjectLabel(groups[i], i)); } if (current && std::ranges::find(refs, current) == refs.end()) { refs.emplace_back(current); names.emplace_back(strex("{} (not in edited system)", GetSparkObjectLabel(current, names.size() - 1))); } int32_t index = 0; for (size_t i = 0; i < refs.size(); i++) { if (refs[i] == current) { index = numeric_cast<int32_t>(i); break; } } if (ImGui::BeginCombo(label, names[numeric_cast<size_t>(index)].c_str())) { for (size_t i = 0; i < names.size(); i++) { bool selected = index == numeric_cast<int32_t>(i); if (ImGui::Selectable(names[i].c_str(), selected)) { Changed |= true; set(refs[i]); } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } } void SparkParticleEditor::Impl::DrawSparkEmitterRef(const char* label, const SPK::Ref<SPK::Emitter>& current, const function<void(const SPK::Ref<SPK::Emitter>&)>& set) { FO_STACK_TRACE_ENTRY(); vector<SPK::Ref<SPK::Emitter>> refs {SPK::Ref<SPK::Emitter>()}; vector<string> names {"None"}; auto groups = GetSparkGroups(); for (size_t group_index = 0; group_index < groups.size(); group_index++) { auto&& group = groups[group_index]; string group_name = GetSparkObjectLabel(group, group_index); for (size_t emitter_index = 0; emitter_index < group->getNbEmitters(); emitter_index++) { auto&& emitter = group->getEmitter(emitter_index); refs.emplace_back(emitter); names.emplace_back(strex("{} / {}", group_name, GetSparkObjectLabel(emitter, emitter_index))); } } if (current && std::ranges::find(refs, current) == refs.end()) { refs.emplace_back(current); names.emplace_back(strex("{} (not in edited system)", GetSparkObjectLabel(current, names.size() - 1))); } int32_t index = 0; for (size_t i = 0; i < refs.size(); i++) { if (refs[i] == current) { index = numeric_cast<int32_t>(i); break; } } if (ImGui::BeginCombo(label, names[numeric_cast<size_t>(index)].c_str())) { for (size_t i = 0; i < names.size(); i++) { bool selected = index == numeric_cast<int32_t>(i); if (ImGui::Selectable(names[i].c_str(), selected)) { Changed |= true; set(refs[i]); } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } } void SparkParticleEditor::Impl::DrawSparkActionAddButtons(const function<void(const SPK::Ref<SPK::Action>&)>& add, const SPK::Ref<SPK::Group>& preferred_group) { FO_STACK_TRACE_ENTRY(); auto target_group = GetSparkFallbackGroup(preferred_group); if (ImGui::Button("Add SpawnParticlesAction")) { add(SPK::SpawnParticlesAction::create(1, 1, target_group, GetFirstSparkEmitter(target_group))); } if (ImGui::Button("Add ActionSet")) { add(SPK::ActionSet::create()); } } auto SparkParticleEditor::Impl::GetEditedSparkSystem() -> SPK::Ref<SPK::System> { FO_STACK_TRACE_ENTRY(); if (!Particle.has_value()) { return {}; } return GetSparkParticleRuntimeSystem(&Particle.value())->GetEditableBaseSystem(); } auto SparkParticleEditor::Impl::GetSparkGroups() -> vector<SPK::Ref<SPK::Group>> { FO_STACK_TRACE_ENTRY(); vector<SPK::Ref<SPK::Group>> groups; if (auto&& system = GetEditedSparkSystem()) { groups.reserve(system->getNbGroups()); for (size_t i = 0; i < system->getNbGroups(); i++) { groups.emplace_back(system->getGroup(i)); } } return groups; } auto SparkParticleEditor::Impl::GetSparkFallbackGroup(const SPK::Ref<SPK::Group>& preferred_group) -> SPK::Ref<SPK::Group> { FO_STACK_TRACE_ENTRY(); if (preferred_group) { return preferred_group; } auto groups = GetSparkGroups(); return !groups.empty() ? groups.front() : SPK::Ref<SPK::Group>(); } auto SparkParticleEditor::Impl::GetFirstSparkEmitter(const SPK::Ref<SPK::Group>& preferred_group) -> SPK::Ref<SPK::Emitter> { FO_STACK_TRACE_ENTRY(); if (preferred_group && preferred_group->getNbEmitters() != 0) { return preferred_group->getEmitter(0); } for (auto&& group : GetSparkGroups()) { if (group->getNbEmitters() != 0) { return group->getEmitter(0); } } return {}; } auto SparkParticleEditor::Impl::GetSparkObjectLabel(const SPK::Ref<SPK::SPKObject>& obj, size_t index) -> string { FO_STACK_TRACE_ENTRY(); if (!obj) { return "None"; } return strex("{} ({})", obj->getName().empty() ? strex("{}", index + 1) : string(obj->getName()), string(obj->getClassName())); } void SparkParticleEditor::Impl::DrawSparkArray(const char* label, bool opened, const function<size_t()>& get_size, const function<const SPK::Ref<SPK::SPKObject>(size_t)>& get, const function<void(size_t)>& del, const function<void()>& add_draw) { FO_STACK_TRACE_ENTRY(); if (ImGui::TreeNodeEx(label, opened ? ImGuiTreeNodeFlags_DefaultOpen : 0, "%s (%d)", label, numeric_cast<int32_t>(get_size()))) { int32_t delIndex = -1; for (size_t i = 0; i < get_size(); i++) { auto&& obj = get(i); string name = strex("{} ({})", obj->getName().empty() ? strex("{}", i + 1) : string(obj->getName()), string(obj->getClassName())); if (ImGui::TreeNodeEx(obj.get(), 0, "%s", name.c_str())) { DrawGenericSparkObject(obj); ImGui::TreePop(); } if (RemovingMode) { if (ImGui::Button(strex("Remove {}", name).c_str())) { delIndex = numeric_cast<int32_t>(i); } } } if (delIndex != -1) { del(delIndex); } if (AddingMode) { auto prev_size = get_size(); add_draw(); Changed |= (get_size() != prev_size); } ImGui::TreePop(); } } void SparkParticleEditor::Impl::DrawSparkNullableField(const char* label, const function<SPK::Ref<SPK::SPKObject>()>& get, const function<void()>& del, const function<void()>& add_draw) { FO_STACK_TRACE_ENTRY(); if (ImGui::TreeNodeEx(label, 0, "%s (%s)", label, get() ? get()->getClassName().c_str() : "Not assigned")) { if (get()) { DrawGenericSparkObject(get()); if (RemovingMode) { if (ImGui::Button("Remove")) { Changed |= true; del(); } } } else { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); ImGui::Text("No %s", label); ImGui::PopStyleColor(); if (AddingMode) { add_draw(); Changed |= !!get(); } } ImGui::TreePop(); } } #undef DRAW_SPK_FLOAT #undef DRAW_SPK_BOOL #undef DRAW_SPK_BOOL_BOOL #undef DRAW_SPK_FLOAT_FLOAT #undef DRAW_SPK_INT #undef DRAW_SPK_INT_INT #undef DRAW_SPK_FLOAT_BOOL #undef DRAW_SPK_VECTOR #undef DRAW_SPK_VECTOR_VECTOR #undef DRAW_SPK_COLOR #undef DRAW_SPK_COLOR_COLOR #undef DRAW_SPK_COMBO #undef DRAW_SPK_COMBO_COMBO #undef DRAW_SPK_COMBO_COMBO_COMBO #undef DRAW_SPK_INTERPOLATOR_FIELD FO_END_NAMESPACE #endif