Add machine workshop, typed logic, mechanics and sensor-driven ECUs

This commit is contained in:
max
2026-11-02 07:27:13 +01:00
parent eb2b6366c2
commit abfcce0f46
99 changed files with 28835 additions and 145 deletions

View File

@@ -0,0 +1,45 @@
#pragma once
#include "FluidSim/Machine.hpp"
#include "FluidSim/Desktop/WorkshopLayout.hpp"
#include <SFML/Graphics.hpp>
#include <functional>
#include <memory>
namespace fluidsim::port::desktop {
struct LogicEditorHooks {
MachineDocument& document;
const MachineSnapshot* live{};
bool shift{};
std::function<bool(const std::function<void()>&)> edit;
std::function<void(std::uint32_t,bool)> boolean_control;
std::function<void(std::uint32_t,double)> number_control;
std::function<void()> release_buttons;
std::function<void(std::uint32_t)> select_physical,focus_physical;
std::function<void(const std::string&)> status;
std::function<void()> example;
};
class LogicEditor {
public:
LogicEditor();
~LogicEditor();
void reset_document();
void deactivate(const LogicEditorHooks&);
bool editing_text()const;
bool active_gesture()const;
std::uint32_t selected_node()const;
void select_node(std::uint32_t,const MachineDocument&);
void select_part_reference(std::uint32_t,const MachineDocument&);
void fit(const LogicGraph&,sf::FloatRect,bool selection_only=false);
void reveal(std::uint32_t,const MachineDocument&,sf::FloatRect);
sf::Vector2f project(LogicPosition,sf::FloatRect)const;
LogicPosition unproject(sf::Vector2f,sf::FloatRect)const;
sf::FloatRect node_rect(const LogicNode&,sf::FloatRect)const;
sf::Vector2f pin_position(const LogicNode&,const std::string&,sf::FloatRect)const;
// Panels can be owned independently in split view; the canvas is always live.
bool event(const sf::Event&,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels);
void draw(sf::RenderWindow&,const sf::Font*,const LogicEditorHooks&,const WorkshopLayout&,sf::FloatRect canvas,bool panels);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include "FluidSim/Machine.hpp"
#include "FluidSim/Desktop/OrbitCamera.hpp"
#include <SFML/Graphics.hpp>
#include <memory>
#include <optional>
namespace fluidsim::port::desktop {
struct BuilderRay {Vec3 origin,direction;};
BuilderRay builder_ray(const OrbitCamera&,sf::FloatRect,sf::Vector2f);
std::optional<sf::Vector2f> builder_project(const OrbitCamera&,sf::FloatRect,Vec3);
// Returns metres along the ray; OBBs and duct centerlines use the rendered dimensions.
std::optional<float> builder_pick_part(const MachinePart&,BuilderRay,float tolerance=0);
Vec3 builder_rotate_world(Vec3 euler,unsigned axis,float degrees);
struct BuilderPipeLocation {std::size_t segment{};Vec3 local_point;float screen_distance{};};
std::optional<BuilderPipeLocation> builder_pipe_location(const OrbitCamera&,sf::FloatRect,const MachinePart&,sf::Vector2f);
struct MachinePlaybackStats {double simulation_seconds{};std::uint64_t underruns{};std::size_t buffered{};double speed{1};bool paused{};};
enum class WorkshopView {ThreeD,Logic,Split};
class MachineBuilder {
public:
MachineBuilder();
~MachineBuilder();
const MachineDocument& document()const;
std::vector<std::uint32_t> selected_parts()const;
void set_document(MachineDocument,std::filesystem::path source={});
bool load_file(const std::filesystem::path&);
void start_test();
void stop_test();
bool testing()const;
MachinePlaybackStats playback_stats()const;
void set_test_speed(double);
void pause_test();
void step_test();
std::vector<MachineMeasurement> measurements()const;
bool export_measurements(const std::filesystem::path&);
bool finished()const;
bool wants_close_window()const;
bool handle_event(const sf::Event&,sf::Vector2u);
void update();
void draw(sf::RenderWindow&,const sf::Font*);
static sf::FloatRect viewport(sf::Vector2u);
sf::FloatRect workspace_viewport(sf::Vector2u)const;
sf::FloatRect logic_viewport(sf::Vector2u)const;
WorkshopView workspace_view()const;
void set_workspace_view(WorkshopView);
std::uint32_t selected_logic_node()const;
std::vector<LogicNodeState> logic_values()const;
sf::FloatRect logic_node_rectangle(std::uint32_t,sf::Vector2u)const;
sf::Vector2f logic_pin_position(std::uint32_t,const std::string&,sf::Vector2u)const;
void restore_workspace_preferences();
const OrbitCamera& camera()const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
void run_machine_builder(sf::RenderWindow&,const sf::Font*,const std::filesystem::path& file={},bool play=false);
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "FluidSim/Machine.hpp"
#include "FluidSim/Desktop/OrbitCamera.hpp"
#include <SFML/Graphics.hpp>
#include <memory>
namespace fluidsim::port::desktop {
struct MachineRenderStats {std::uint64_t pipe_mesh_builds{},icon_atlas_builds{};std::size_t cached_pipes{};};
class MachineRenderer3D {
public:
MachineRenderer3D();
~MachineRenderer3D();
MachineRenderer3D(const MachineRenderer3D&)=delete;
MachineRenderer3D& operator=(const MachineRenderer3D&)=delete;
void draw(sf::RenderWindow&,const MachineDocument&,const MachineSnapshot*,sf::FloatRect,const OrbitCamera&,std::uint32_t selected,const MachinePart* ghost=nullptr,const std::vector<std::uint32_t>& selection={});
void draw_icon(sf::RenderWindow&,MachinePartKind,sf::FloatRect);
MachineRenderStats stats()const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "FluidSim/MachinePorts.hpp"
#include <SFML/Graphics/Color.hpp>
namespace fluidsim::port::desktop {
inline sf::Color port_color(PortDomain domain){
switch(domain){case PortDomain::Boolean:return {235,92,102};case PortDomain::Number:return {87,201,128};case PortDomain::Gas:return {189,200,214};case PortDomain::Rotational:return {229,184,91};case PortDomain::Translational:return {177,139,220};case PortDomain::EcuAssignment:return {91,161,242};}
return sf::Color::White;
}
inline bool filled_port(const MachinePortDescriptor& descriptor){return descriptor.direction==PortDirection::Output;}
}

View File

@@ -2,6 +2,26 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include <SFML/Graphics.hpp>
#include <optional>
#include <memory>
namespace fluidsim::port::desktop {
std::optional<std::size_t> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
struct ScenarioMenuChoice {
std::optional<std::size_t> example;
std::filesystem::path machine;
bool play{};
};
enum class MenuScreen {Home,Workshop,Scenarios};
class ScenarioMenu {
public:
ScenarioMenu(std::vector<ScenarioExample>,std::vector<std::filesystem::path> machines={});
~ScenarioMenu();
bool handle_event(const sf::Event&,sf::Vector2u);
void draw(sf::RenderWindow&,const sf::Font*);
bool finished()const;
MenuScreen screen()const;
std::optional<ScenarioMenuChoice> take_choice();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
std::optional<ScenarioMenuChoice> choose_scenario(sf::RenderWindow&,const sf::Font*,const std::vector<ScenarioExample>&);
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
namespace fluidsim::port::desktop {
// Maximize the decorated OS window; retain normal restore/minimize controls.
void maximize_window(sf::RenderWindow&);
bool window_is_maximized(const sf::RenderWindow&);
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <SFML/Graphics/Rect.hpp>
#include <SFML/System/Vector2.hpp>
#include <algorithm>
namespace fluidsim::port::desktop {
// One geometry source for drawing, picking and viewport projection.
struct WorkshopLayout {
sf::FloatRect left, right, viewport, library, search;
float inspector_x{}, inspector_width{}, height{}, width{};
static constexpr float field_top=302, field_height=52;
int field_rows()const{return std::max(1,int((height-164-field_top)/field_height));}
int library_rows()const{return std::max(1,int(library.size.y/36));}
sf::FloatRect field_row(int row)const{return {{inspector_x+12,field_top+row*field_height},{inspector_width-24,48}};}
sf::FloatRect field_value(int row)const{auto r=field_row(row);return {{r.position.x,r.position.y+21},{r.size.x-68,26}};}
static WorkshopLayout calculate(sf::Vector2u size,float left_width=260,float right_width=340,bool left_closed=false,bool right_closed=false){
WorkshopLayout l;l.width=float(size.x);l.height=float(size.y);
const float available=std::max(480.f,l.width-440);
float lw=left_closed?28.f:std::clamp(left_width,220.f,420.f),rw=right_closed?28.f:std::clamp(right_width,280.f,520.f);
if(lw+rw>available){const float excess=lw+rw-available;rw=std::max(right_closed?28.f:280.f,rw-excess);lw=std::max(left_closed?28.f:180.f,std::min(lw,available-rw));}
l.left={{8,96},{lw,std::max(100.f,l.height-154)}};
l.right={{l.width-rw-8,96},{rw,std::max(100.f,l.height-154)}};
l.inspector_x=l.right.position.x;l.inspector_width=rw;
l.viewport={{l.left.position.x+lw+8,96},{std::max(100.f,l.right.position.x-lw-24),std::max(100.f,l.height-154)}};
l.search={{20,152},{lw-24,30}};
l.library={{20,230},{lw-24,std::max(36.f,l.height-474)}};
return l;
}
};
}