Add machine workshop, typed logic, mechanics and sensor-driven ECUs
This commit is contained in:
86
tests/logic_editor_tests.cpp
Normal file
86
tests/logic_editor_tests.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "FluidSim/Desktop/LogicEditor.hpp"
|
||||
#include "FluidSim/Desktop/PortAppearance.hpp"
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
using namespace fluidsim::port::desktop;
|
||||
namespace {
|
||||
void check(bool condition,const char* why){if(!condition)throw std::runtime_error(why);}
|
||||
bool close(double a,double b,double tolerance=1e-5){return std::abs(a-b)<tolerance;}
|
||||
struct Fixture {
|
||||
MachineDocument doc;LogicEditor editor;std::unique_ptr<MachineSimulation> simulation;MachineSnapshot live;
|
||||
sf::Vector2u size{1920,1080};WorkshopLayout ui=WorkshopLayout::calculate(size);sf::FloatRect canvas=ui.viewport;
|
||||
std::vector<MachineDocument> history;std::string status;std::uint32_t physical{},focused{};bool shift{};
|
||||
LogicEditorHooks hooks(){return {doc,simulation?&live:nullptr,shift,
|
||||
[this](const std::function<void()>& action){const auto before=doc;try{action();validate_machine(doc);if(simulation)simulation->apply_tuning(doc);history.push_back(before);return true;}catch(const std::exception& e){doc=before;status=e.what();return false;}},
|
||||
[this](std::uint32_t id,bool value){simulation->set_logic_control(id,value);},[this](std::uint32_t id,double value){simulation->set_logic_control(id,value);},[this]{if(simulation)simulation->release_logic_buttons();},
|
||||
[this](std::uint32_t id){physical=id;},[this](std::uint32_t id){focused=id;},[this](const std::string& text){status=text;},[this]{load(machine_logic_example());}};}
|
||||
void load(MachineDocument d){simulation.reset();doc=std::move(d);editor.reset_document();editor.fit(doc.logic,canvas);history.clear();}
|
||||
void send(sf::Event e){auto h=hooks();editor.event(e,h,ui,canvas,true);}
|
||||
void key(sf::Keyboard::Key code,bool control=false){send(sf::Event::KeyPressed{.code=code,.control=control});}
|
||||
void text(const std::string& value){for(const auto c:value)send(sf::Event::TextEntered{std::uint32_t(c)});}
|
||||
void press(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){send(sf::Event::MouseButtonPressed{button,{int(std::round(point.x)),int(std::round(point.y))}});}
|
||||
void release(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){send(sf::Event::MouseButtonReleased{button,{int(std::round(point.x)),int(std::round(point.y))}});}
|
||||
void click(sf::Vector2f point,sf::Mouse::Button button=sf::Mouse::Button::Left){press(point,button);release(point,button);}
|
||||
void drag(sf::Vector2f from,sf::Vector2f to){press(from);send(sf::Event::MouseMoved{{int(std::round(to.x)),int(std::round(to.y))}});release(to);}
|
||||
sf::Vector2f pin(std::uint32_t id,const char* key){return editor.pin_position(*doc.logic.find(id),key,canvas);}
|
||||
sf::Vector2f control(std::uint32_t id,float fraction=.5f){const auto r=editor.node_rect(*doc.logic.find(id),canvas);const float zoom=r.size.x/240;return {r.position.x+(12+216*fraction)*zoom,r.position.y+r.size.y-26*zoom};}
|
||||
void start(){simulation=std::make_unique<MachineSimulation>(doc);step();}
|
||||
void step(int count=1){for(int i=0;i<count;++i)simulation->process();live=simulation->snapshot();}
|
||||
SignalValue value(std::uint32_t id,const char* key){const auto ports=logic_node_ports(*doc.logic.find(id));for(const auto& n:live.logic)if(n.id==id)for(std::size_t i=0;i<ports.size();++i)if(ports[i].key==key)return n.values[i];throw std::runtime_error("Missing live pin");}
|
||||
};
|
||||
}
|
||||
int main(int argc,char** argv)try{
|
||||
Fixture f;
|
||||
{
|
||||
f.load(machine_speed_density_example());std::uint32_t connector=0;for(const auto& n:f.doc.logic.nodes)if(n.kind==LogicNodeKind::Ecu)connector=n.id;
|
||||
const auto pins=logic_node_ports(*f.doc.logic.find(connector));check(pins.size()==4&&pins[2].key=="map"&&pins[2].pressure_reference==PressureReference::Absolute&&pins[3].key=="iat","Speed-density canvas connector has the wrong sensor schema");
|
||||
f.editor.reveal(connector,f.doc,f.canvas);f.click(f.editor.node_rect(*f.doc.logic.find(connector),f.canvas).position+sf::Vector2f{30,12});f.start();check(f.value(connector,"map").validity==SignalValidity::Valid&&f.value(connector,"iat").validity==SignalValidity::Valid,"Live speed-density canvas lost MAP/IAT values");
|
||||
}
|
||||
{
|
||||
f.load(machine_sensor_engine_example(true));std::uint32_t connector=0,target=0;for(const auto& n:f.doc.logic.nodes)if(n.kind==LogicNodeKind::Ecu){connector=n.id;target=n.part;}
|
||||
const auto ports=logic_node_ports(*f.doc.logic.find(connector));check(ports.size()==3&&ports[0].quantity==SignalQuantity::AngularSpeed&&ports[1].quantity==SignalQuantity::Angle&&ports[2].quantity==SignalQuantity::Fraction,"Alpha-N connector lost required typed sensor inputs");for(const auto& pin:ports)check(pin.direction==PortDirection::Input&&pin.required,"ECU inputs are not required ring ports");
|
||||
f.editor.reveal(connector,f.doc,f.canvas);f.click(f.editor.node_rect(*f.doc.logic.find(connector),f.canvas).position+sf::Vector2f{30,12});check(f.physical==target,"ECU graph selection did not select physical controller");
|
||||
f.key(sf::Keyboard::Key::C,true);f.key(sf::Keyboard::Key::V,true);const auto pasted=f.editor.selected_node();check(pasted!=connector&&f.doc.logic.find(pasted)->kind==LogicNodeKind::Ecu&&f.doc.logic.find(pasted)->part==0,"Copied ECU connector stole existing ownership");
|
||||
}
|
||||
{
|
||||
f.load({});f.key(sf::Keyboard::Key::F,true);f.text("Hysteresis");f.key(sf::Keyboard::Key::Enter);f.click({80,244});const auto point=f.canvas.position+sf::Vector2f{120,140};f.click(point);check(f.doc.logic.nodes.size()==1&&f.doc.logic.nodes[0].kind==LogicNodeKind::Hysteresis,"Searchable node library did not create the chosen kind");check(f.history.size()==1,"Node creation did not enter undo history");
|
||||
}
|
||||
MachineDocument wired;const auto a=wired.logic.add(LogicNodeKind::Switch,{0,0}).id,b=wired.logic.add(LogicNodeKind::Switch,{0,250}).id,gate=wired.logic.add(LogicNodeKind::And,{420,80}).id,number=wired.logic.add(LogicNodeKind::NumberConstant,{420,360}).id;f.load(wired);
|
||||
f.click(f.pin(a,"out"));f.click(f.pin(gate,"in1"));check(f.doc.logic.links.size()==1,"Click-to-connect failed");f.drag(f.pin(b,"out"),f.pin(gate,"in2"));check(f.doc.logic.links.size()==2,"Drag-to-connect failed");
|
||||
const auto before=f.doc.logic.links;f.click(f.pin(number,"out"));f.click(f.pin(gate,"in1"));check(f.doc.logic.links==before&&!f.status.empty(),"Incompatible preview/edit changed existing wiring");f.key(sf::Keyboard::Key::Escape);
|
||||
f.drag(f.pin(gate,"in2"),f.pin(a,"out"));check(f.doc.logic.links.size()==2&&f.doc.logic.links.back().from.node==a,"Rewiring a connected input retained its old driver or rejected fan-out");
|
||||
const auto rewired=f.doc.logic.links;f.drag(f.pin(gate,"in2"),f.pin(number,"out"));check(f.doc.logic.links==rewired,"Rejected rewire disconnected the original signal");f.key(sf::Keyboard::Key::Escape);f.click(f.pin(gate,"in1"),sf::Mouse::Button::Right);check(f.doc.logic.links.size()==1,"Right-click input did not disconnect its driver");
|
||||
{
|
||||
const auto old=f.doc.logic.find(a)->position;const auto r=f.editor.node_rect(*f.doc.logic.find(a),f.canvas);const auto from=r.position+sf::Vector2f{30,12},to=from+sf::Vector2f{80,25};const auto p=f.editor.unproject(from,f.canvas),q=f.editor.unproject(to,f.canvas);const auto count=f.history.size();f.drag(from,to);check(close(f.doc.logic.find(a)->position.x-old.x,q.x-p.x,2)&&close(f.doc.logic.find(a)->position.y-old.y,q.y-p.y,2),"Header drag did not move the selected graph node");check(f.history.size()==count+1,"A node drag created more than one undo entry");
|
||||
const auto anchor=f.canvas.position+f.canvas.size*.4f;const auto original=f.editor.unproject(anchor,f.canvas);f.send(sf::Event::MouseWheelScrolled{sf::Mouse::Wheel::Vertical,2,{int(anchor.x),int(anchor.y)}});const auto after=f.editor.unproject(anchor,f.canvas);check(close(original.x,after.x,1)&&close(original.y,after.y,1),"Wheel zoom did not preserve the point beneath the pointer");
|
||||
}
|
||||
{
|
||||
MachineDocument doc;const auto button=doc.logic.add(LogicNodeKind::Button,{0,0}).id,lamp=doc.logic.add(LogicNodeKind::Lamp,{420,0}).id;doc.logic.connect({button,"out"},{lamp,"in"});f.load(doc);f.start();f.press(f.control(button));f.step();check(std::get<bool>(f.value(lamp,"in").value),"Live momentary control did not reach the graph");f.send(sf::Event::FocusLost{});f.step();check(!std::get<bool>(f.value(lamp,"in").value),"Focus loss left a momentary button true");f.press(f.control(button));auto h=f.hooks();f.editor.deactivate(h);f.step();check(!std::get<bool>(f.value(lamp,"in").value),"Closing the graph left a held button active");
|
||||
const auto nodes=f.doc.logic.nodes.size();f.key(sf::Keyboard::Key::Delete);check(f.doc.logic.nodes.size()==nodes,"Test allowed node deletion");
|
||||
}
|
||||
{
|
||||
MachineDocument doc;const auto toggle=doc.logic.add(LogicNodeKind::Switch,{0,0}).id,lamp=doc.logic.add(LogicNodeKind::Lamp,{420,0}).id;doc.logic.connect({toggle,"out"},{lamp,"in"});f.load(doc);f.start();const auto time=f.live.time;f.click(f.control(toggle));f.click(f.control(toggle));check(f.live.time==time,"Editing controls advanced a paused simulation");f.step();check(!std::get<bool>(f.value(lamp,"in").value)&&!f.doc.logic.find(toggle)->boolean,"Two queued toggles collapsed or changed configured initial state");
|
||||
}
|
||||
{
|
||||
MachineDocument doc;const auto slider=doc.logic.add(LogicNodeKind::Slider,{0,0}).id,readout=doc.logic.add(LogicNodeKind::Readout,{420,0}).id;doc.logic.connect({slider,"out"},{readout,"in"});f.load(doc);f.start();f.click(f.control(slider,.75f));f.step();check(close(std::get<double>(f.value(readout,"in").value),.75,.01)&&f.doc.logic.find(slider)->value==0,"Live slider failed or changed its initial value");
|
||||
}
|
||||
{
|
||||
MachineDocument doc;const auto constant=doc.logic.add(LogicNodeKind::NumberConstant,{0,0}).id,readout=doc.logic.add(LogicNodeKind::Readout,{420,0}).id;doc.logic.connect({constant,"out"},{readout,"in"});f.load(doc);f.start();f.editor.select_node(constant,f.doc);const auto time=f.live.time;f.click({f.ui.inspector_x+40,342});f.text("-3.25");f.key(sf::Keyboard::Key::Enter);check(f.doc.logic.find(constant)->value==-3.25&&f.live.time==time,"Live constant property edit failed or restarted physics");f.step();check(std::get<double>(f.value(readout,"in").value)==-3.25,"Live constant edit did not reach the next sample");f.click({f.ui.inspector_x+40,342});f.text("1e999");f.key(sf::Keyboard::Key::Enter);check(f.editor.editing_text()&&f.doc.logic.find(constant)->value==-3.25,"Invalid numeric input was lost or applied");f.key(sf::Keyboard::Key::Escape);
|
||||
}
|
||||
{
|
||||
MachineDocument doc;const auto host=doc.add(MachinePartKind::Chamber).id,sensor=doc.add_sensor(MachineSensorKind::Pressure,host).id,reference=doc.logic.add(LogicNodeKind::Sensor,{0,0}).id;f.load(doc);f.editor.select_node(reference,f.doc);f.click({f.ui.inspector_x+60,208});check(f.doc.logic.find(reference)->sensor==sensor&&f.doc.sensors.size()==1&&f.doc.parts.size()==1,"Sensor reference created a duplicate physical sensor or failed to bind");f.click({f.ui.inspector_x+60,f.ui.height-92});check(f.focused==host,"Focus in 3D did not resolve the shared physical host");
|
||||
f.key(sf::Keyboard::Key::C,true);f.key(sf::Keyboard::Key::V,true);check(f.doc.logic.nodes.size()==2&&f.doc.logic.nodes.back().sensor==sensor,"Copied sensor node did not reference the same sensor entity");
|
||||
}
|
||||
{
|
||||
MachineDocument destination;const auto part=destination.add(MachinePartKind::Chamber).id;destination.add_sensor(MachineSensorKind::GasTemperature,part);f.load(destination);f.key(sf::Keyboard::Key::V,true);check(f.doc.logic.nodes.size()==1&&f.doc.logic.nodes[0].sensor==0&&!f.doc.logic.nodes[0].binding_label.empty(),"Cross-document graph paste silently matched a coincidental sensor ID or lost its intended binding");
|
||||
}
|
||||
if(argc==3&&std::string(argv[1])=="--ui-preview"){
|
||||
const std::filesystem::path output=argv[2];std::filesystem::create_directories(output);sf::ContextSettings settings;settings.depthBits=24;settings.majorVersion=2;settings.minorVersion=1;sf::RenderWindow window(sf::VideoMode({1920,1080}),"Logic editor review",sf::Style::Default,sf::State::Windowed,settings);window.setVisible(false);sf::Font font("C:/Windows/Fonts/consola.ttf");
|
||||
{MachineDocument shapes;const auto source=shapes.logic.add(LogicNodeKind::Switch,{0,0}).id,input=shapes.logic.add(LogicNodeKind::Lamp,{420,0}).id,number=shapes.logic.add(LogicNodeKind::NumberConstant,{0,250}).id,readout=shapes.logic.add(LogicNodeKind::Readout,{420,250}).id;shapes.logic.connect({source,"out"},{input,"in"});shapes.logic.connect({number,"out"},{readout,"in"});f.load(shapes);window.clear();auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);sf::Texture pixels(f.size);pixels.update(window);const auto image=pixels.copyToImage();const auto sample=[&](std::uint32_t id,const char* key){const auto at=f.pin(id,key);return image.getPixel({unsigned(std::round(at.x)),unsigned(std::round(at.y))});};const auto red=sample(source,"out"),green=sample(number,"out"),ring=sample(input,"in");check(red.r>150&&red.g<150&&green.g>150&&green.r<150,"Rendered signal output circles do not use the declared Boolean/Number colors");check(ring.r<50&&ring.g<50&&ring.b<50,"Rendered input pin has a filled center instead of a ring");}
|
||||
for(const auto resolution:{sf::Vector2u{1920,1080},sf::Vector2u{1280,720}}){f.size=resolution;f.ui=WorkshopLayout::calculate(resolution);f.canvas=f.ui.viewport;window.setSize(resolution);f.load(machine_logic_example());auto capture=[&](const std::string& name){window.clear();auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);sf::Texture pixels(resolution);pixels.update(window);check(pixels.copyToImage().saveToFile(output/(std::to_string(resolution.x)+"-"+name+".png")),"Logic screenshot save failed");};capture("graph-build");f.start();for(int frame=0;frame<10;++frame){f.step(1200);auto h=f.hooks();f.editor.draw(window,&font,h,f.ui,f.canvas,true);}capture("graph-live");for(const auto& n:f.doc.logic.nodes)if(n.kind==LogicNodeKind::Hysteresis){f.editor.reveal(n.id,f.doc,f.canvas);capture("threshold-inspector");break;}}
|
||||
}
|
||||
std::cout<<"Logic canvas placement, typed wiring/rewiring, undo, controls, focus release, binding and live inspector passed\n";return 0;
|
||||
}catch(const std::exception& e){std::cerr<<e.what()<<'\n';return 1;}
|
||||
Reference in New Issue
Block a user