#pragma once #include "FluidSim/Scenario.hpp" #include "FluidSim/ScenarioCatalog.hpp" #include "FluidSim/TwoStrokeScenario.hpp" #include #include namespace fluidsim::port { // Shared desktop controls and snapshots. Each simulation owns its state lock. class DesktopScenario { public: explicit DesktopScenario(const ScenarioExample& e) : DesktopScenario(e.kind,e.two,e.exhaust,e.junctions,e.engine,e.title) {} explicit DesktopScenario(ScenarioKind kind, TwoStrokeConfig config = {}, ExhaustConfig exhaust = {}, JunctionConfig junctions = {}, EngineConfig engine = {}, std::string name = {}) : name_(std::move(name)),exhaust_(exhaust),junctions_(junctions) { if (kind == ScenarioKind::TwoStroke) two_ = std::make_unique(44100, config); else { if(!engine.cylinders)engine=preset_engine_definition(kind==ScenarioKind::SaabB205? FourStrokePreset::SaabB205:kind==ScenarioKind::AmericanV8?FourStrokePreset::AmericanV8:FourStrokePreset::Twin).engine; four_ = std::make_unique(44100,std::move(engine),exhaust,junctions); } } AudioFrame process() { const std::lock_guard lock(mutex_); if (four_) return four_->process(); return two_->process(); } void set_controls(float pedal, float load, float clutch) { visit([&](auto& scenario) { pedal_=pedal;load_=load;clutch_=clutch;scenario.set_controls(pedal, load, clutch); }); } void set_starter(bool engaged) { visit([&](auto& scenario) { starter_=engaged;scenario.set_starter(engaged); }); } void shift_up() { visit([](auto& scenario) { scenario.shift_up(); }); } void shift_down() { visit([](auto& scenario) { scenario.shift_down(); }); } void set_neutral() { visit([](auto& scenario) { scenario.set_neutral(); }); } void set_pipe_solver(PipeSolver solver) { visit([&](auto& scenario) { solver_=solver;scenario.set_pipe_solver(solver); }); } void set_audio_scene(OutdoorScene scene) { visit([&](auto& scenario) { scene_=scene;scenario.set_audio_scene(scene); }); } void set_listener(AudioListener listener) { visit([&](auto& scenario) { listener_=listener;scenario.set_listener(listener); }); } void set_tuning(const EngineConfig& tuning) { const std::lock_guard lock(mutex_); if(four_)four_->set_tuning(tuning); } // Build on a background thread. Publishing takes the same lock as process(), // so no solver/port pointers can be observed during the swap. void rebuild_engine(const EngineConfig& engine) { validate_engine_config(engine,"pipe rebuild"); auto replacement=std::make_unique(44100,engine,exhaust_,junctions_); { const std::lock_guard lock(mutex_); if(!four_)throw std::invalid_argument("Live editor requires a four-stroke engine"); replacement->set_controls(pedal_,load_,clutch_); replacement->set_starter(starter_);replacement->set_pipe_solver(solver_); replacement->set_audio_scene(scene_);replacement->set_listener(listener_); four_.swap(replacement); } } void set_cells_per_meter(float density) { mesh_cell_count(.1f,density); EngineConfig engine;TwoStrokeConfig two_config;bool is_two; { const std::lock_guard lock(mutex_); is_two=bool(two_); if(is_two) two_config=two_->configuration; else engine=four_->engine_config; } std::unique_ptr next_four; std::unique_ptr next_two; if(is_two) { two_config.cells_per_meter=density;two_config.exhaust_cell_length=1/density;next_two=std::make_unique(44100,two_config); } else { engine.cells_per_meter=density;next_four=std::make_unique(44100,engine,exhaust_,junctions_); } { const std::lock_guard lock(mutex_); const auto restore=[&](auto& scenario) { scenario.set_controls(pedal_,load_,clutch_);scenario.set_starter(starter_); scenario.set_pipe_solver(solver_);scenario.set_audio_scene(scene_);scenario.set_listener(listener_); }; if(is_two) {restore(*next_two);two_.swap(next_two);} else {restore(*next_four);four_.swap(next_four);} } } ScenarioSnapshot snapshot() const { const std::lock_guard lock(mutex_); if (four_) { auto s=four_->snapshot();s.example_name=name_;s.cells_per_meter=four_->engine_config.cells_per_meter;return s; } const auto source = two_->snapshot(); ScenarioSnapshot s; s.example_name=name_; s.pipe_solver=two_->pipes.front().solver; s.two_stroke = true; s.cells_per_meter=two_->configuration.cells_per_meter; s.cylinder_angles[0]=two_->crankshaft.angle; s.cylinder_count = 1; s.racing_exhaust = two_->configuration.exhaust == TwoStrokeExhaust::Racing; s.rpm = source.rpm; s.speed_kmh = two_->vehicle.speed * 3.6f; s.kinetic_energy = two_->vehicle.kinetic_energy(); s.gear = two_->vehicle.current_gear; s.clutch = two_->clutch; s.average_power = two_->crankshaft.average_power; s.average_torque = two_->crankshaft.average_torque; s.cylinder_pressure1 = source.cylinder_pressure; s.cylinder_temperature1 = two_->cylinder.temperature(); s.cylinder_piston_fraction1 = (two_->cylinder.volume - two_->cylinder.clearance_volume()) / two_->cylinder.swept_volume(); s.crankcase_pressure = source.crankcase_pressure; s.intake_pressure = source.intake_pressure; s.exhaust_pressure = two_->pipes[3].state(0).pressure; s.throttle = source.throttle; s.pedal = source.pedal; s.target_afr = two_->carburetor.indicated_afr(); s.valve1_intake_area = source.transfer_area; s.valve1_exhaust_area = source.exhaust_area; s.reed_open = source.reed_open; s.fuel_cut = source.fuel_cut; s.ignition_cut = source.ignition_cut; s.idle_control = two_->ecu.idle_active(); s.rev_limit1 = two_->ecu.rev_limited(); s.audio_scene = source.audio_scene; s.intake_flow = two_->intake_flow; s.exhaust_flow = two_->exhaust_flow; s.capture_pipes(two_->pipes); two_->capture_geometry(s); return s; } private: template void visit(Function&& function) { const std::lock_guard lock(mutex_); if (four_) function(*four_); else function(*two_); } std::string name_; ExhaustConfig exhaust_; JunctionConfig junctions_; float pedal_{},load_{},clutch_{}; bool starter_{}; PipeSolver solver_{PipeSolver::Rusanov}; OutdoorScene scene_{OutdoorScene::Wall}; AudioListener listener_{}; std::unique_ptr four_; std::unique_ptr two_; mutable std::mutex mutex_; }; } // namespace fluidsim::port