17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-06 04:38:37 +01:00

Add vehicle graph core: .vhg loader, logic and cab blocks, -graph-run and -graph-catalog

This commit is contained in:
2026-10-13 10:55:53 +02:00
parent c26041f2e7
commit 70abf879b3
42 changed files with 4693 additions and 1 deletions

View File

@@ -0,0 +1,381 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/blocks/cab.h"
#include "vehicle/graph/graph_catalog.h"
#include <algorithm>
#include <cmath>
namespace vgraph
{
namespace
{
std::vector<std::string> split_labels(std::string const &Text)
{
std::vector<std::string> result;
if (Text.empty())
{
return result;
}
std::string::size_type start{0};
while (true)
{
auto const separator{Text.find(';', start)};
result.emplace_back(Text.substr(start, separator == std::string::npos ? std::string::npos : separator - start));
if (separator == std::string::npos)
{
break;
}
start = separator + 1;
}
return result;
}
} // namespace
// cab_switch
cab_switch::cab_switch(block_setup const &Setup)
: block(Setup), m_behaviour(static_cast<behaviour_type>(choice(Setup, param::behaviour))),
m_positioncount(std::max(1, static_cast<int>(std::lround(number(Setup, param::positions))))), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)),
m_spring(boolean(Setup, param::spring)), m_labels(split_labels(text(Setup, param::labels))), m_animation(static_cast<animation_type>(choice(Setup, param::animation))),
m_step(number(Setup, param::step)), m_offset(number(Setup, param::offset)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_keyincrease(text(Setup, param::key_inc)), m_keydecrease(text(Setup, param::key_dec)), m_command(text(Setup, param::command)), m_hardwarename(text(Setup, param::hw_name))
{
// the default is given as a value, as the author sees it; the switch starts at the position closest to it
auto const defaultvalue{number(Setup, param::default_value)};
if (m_positioncount > 1 && m_max != m_min)
{
m_defaultposition = clamp_position(static_cast<int>(std::lround((defaultvalue - m_min) / (m_max - m_min) * (m_positioncount - 1))));
}
m_position = m_defaultposition;
}
int cab_switch::clamp_position(int const Position) const
{
return std::clamp(Position, 0, m_positioncount - 1);
}
double cab_switch::value_at(int const Position) const
{
if (m_positioncount < 2)
{
return m_min;
}
return m_min + (m_max - m_min) * clamp_position(Position) / (m_positioncount - 1);
}
std::string cab_switch::label(int const Position) const
{
return (Position >= 0 && Position < static_cast<int>(m_labels.size()) ? m_labels[Position] : std::string{});
}
void cab_switch::update(frame &Frame)
{
set(Frame, port::value, value_at(m_position));
set(Frame, port::index, m_position);
}
void cab_switch::increase()
{
m_position = clamp_position(m_position + 1);
}
void cab_switch::decrease()
{
m_position = clamp_position(m_position - 1);
}
void cab_switch::toggle()
{
switch (m_behaviour)
{
case behaviour_type::monostable:
{
// a momentary switch can't stay toggled; a toggle is treated as pressing it, the release brings it back
press();
break;
}
default:
{
auto const other{m_defaultposition == m_positioncount - 1 ? 0 : m_positioncount - 1};
m_position = (m_position == m_defaultposition ? other : m_defaultposition);
break;
}
}
}
void cab_switch::press()
{
switch (m_behaviour)
{
case behaviour_type::multi:
{
// levers are moved with increase and decrease; pressing them has no meaning
break;
}
case behaviour_type::monostable:
{
m_position = (m_defaultposition == m_positioncount - 1 ? 0 : m_positioncount - 1);
break;
}
default:
{
toggle();
break;
}
}
}
void cab_switch::release()
{
if (m_behaviour == behaviour_type::monostable || (m_behaviour == behaviour_type::multi && m_spring))
{
m_position = m_defaultposition;
}
}
void cab_switch::set_position(int const Position)
{
m_position = clamp_position(Position);
}
void cab_switch::set_value(double const Value)
{
if (m_positioncount < 2 || m_max == m_min)
{
return;
}
set_position(static_cast<int>(std::lround((Value - m_min) / (m_max - m_min) * (m_positioncount - 1))));
}
block_type cab_switch::describe()
{
block_type type;
type.id = "cab.switch";
type.category = "cab";
type.name = "Switch";
type.description = "Switch, button or lever of the cab. Its behaviour and animation are defined here, and its value can be connected to any block.";
type.origin = block_origin::cab;
type.ports = {signal_out("pos", value_type::real), signal_out("index", value_type::integer)};
type.params = {
choice_param("behaviour", {"bistable", "monostable", "multi"}, "bistable"),
number_param("positions", 2.0),
number_param("min", 0.0),
number_param("max", 1.0),
number_param("default", 0.0),
boolean_param("spring", false),
text_param("labels"),
choice_param("animation", {"rotate", "move"}, "rotate"),
number_param("step", 45.0, "deg/mm"),
number_param("offset", 0.0, "deg/mm"),
number_param("smoothing", 0.2, "s"),
text_param("submodel"),
text_param("key_inc"),
text_param("key_dec"),
text_param("command"),
text_param("hw_name"),
};
type.create = make_block<cab_switch>;
return type;
}
// cab_lamp
cab_lamp::cab_lamp(block_setup const &Setup)
: block(Setup), m_supplied(is_connected(Setup, port::supply)), m_blink(boolean(Setup, param::blink)), m_blinkperiod(std::max(0.01, number(Setup, param::blink_period))),
m_minvoltage(number(Setup, param::min_voltage)), m_power(std::max(0.0, number(Setup, param::power))), m_submodel(text(Setup, param::submodel))
{
}
void cab_lamp::update(frame &Frame)
{
// without low voltage simulation, or without the supply connected, the lamp is assumed to be powered
auto const lowvoltage{Frame.low_voltage && m_supplied};
auto const voltage{lowvoltage ? forward_in(Frame, port::supply) : 0.0};
auto const powered{false == lowvoltage || voltage >= m_minvoltage};
auto lit{powered && get_bool(Frame, port::on)};
if (lit && m_blink)
{
// blinking follows the time of the graph, so all lamps of the vehicle blink in step
lit = std::fmod(Frame.time, m_blinkperiod) < 0.5 * m_blinkperiod;
}
m_lit = lit;
if (lowvoltage && m_lit && voltage > 1.0)
{
add_back(Frame, port::supply, m_power / voltage);
}
}
block_type cab_lamp::describe()
{
block_type type;
type.id = "fb.lamp";
type.category = "indicators";
type.name = "Lamp";
type.description = "Indicator lamp. With low voltage simulation enabled and the supply connected, it needs enough voltage to light, and draws current.";
type.origin = block_origin::cab;
type.ports = {signal_in("on", value_type::boolean), physical_in("supply", domain::low_voltage, true)};
type.params = {
choice_param("color", {"green", "yellow", "red", "white", "blue"}, "green"),
boolean_param("blink", false),
number_param("blink_period", 1.0, "s"),
number_param("min_voltage", 70.0, "V"),
number_param("power", 2.0, "W"),
text_param("submodel"),
};
type.create = make_block<cab_lamp>;
return type;
}
// cab_gauge
cab_gauge::cab_gauge(block_setup const &Setup, bool const Twoneedles)
: block(Setup), m_twoneedles(Twoneedles), m_min(number(Setup, param::min)), m_max(number(Setup, param::max)), m_anglemin(number(Setup, param::angle_min)),
m_anglemax(number(Setup, param::angle_max)), m_smoothing(number(Setup, param::smoothing)), m_submodel(text(Setup, param::submodel)),
m_submodel2(Twoneedles ? text(Setup, param::submodel2) : std::string{})
{
m_values[0] = m_values[1] = static_cast<float>(std::min(m_min, m_max));
}
void cab_gauge::update(frame &Frame)
{
// the needle stops at the ends of the scale
auto const low{std::min(m_min, m_max)};
auto const high{std::max(m_min, m_max)};
m_values[0] = static_cast<float>(std::clamp(get(Frame, port::value), low, high));
if (m_twoneedles)
{
m_values[1] = static_cast<float>(std::clamp(get(Frame, port::value2), low, high));
}
}
block_type cab_gauge::describe(bool const Twoneedles)
{
block_type type;
type.category = "indicators";
type.origin = block_origin::cab;
if (Twoneedles)
{
type.id = "fb.manometer";
type.name = "Two needle gauge";
type.description = "Gauge with two needles, e.g. pressure gauge of the brake pipe (black) and the main reservoir (red).";
type.ports = {signal_in("black", value_type::real, 0.0, "bar"), signal_in("red", value_type::real, 0.0, "bar", true)};
}
else
{
type.id = "fb.gauge";
type.name = "Gauge";
type.description = "Needle gauge: voltmeter, ammeter, speedometer.";
type.ports = {signal_in("value", value_type::real)};
}
type.params = {
number_param("min", 0.0),
number_param("max", Twoneedles ? 10.0 : 100.0),
text_param("unit", Twoneedles ? "bar" : ""),
number_param("angle_min", -135.0, "deg"),
number_param("angle_max", 135.0, "deg"),
number_param("smoothing", 0.3, "s"),
text_param("submodel"),
};
if (Twoneedles)
{
type.params.push_back(text_param("submodel2"));
}
if (Twoneedles)
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, true); };
}
else
{
type.create = [](block_setup const &Setup) -> std::unique_ptr<block> { return std::make_unique<cab_gauge>(Setup, false); };
}
return type;
}
// vehicle_sound
vehicle_sound::vehicle_sound(block_setup const &Setup)
: block(Setup), m_file(text(Setup, param::file)), m_basevolume(number(Setup, param::volume)), m_basepitch(number(Setup, param::pitch)), m_loop(boolean(Setup, param::loop)),
m_placement(static_cast<placement_type>(choice(Setup, param::placement))), m_range(number(Setup, param::range)),
m_offset{number(Setup, param::offset_x), number(Setup, param::offset_y), number(Setup, param::offset_z)}
{
}
void vehicle_sound::update(frame &Frame)
{
m_playing = get_bool(Frame, port::play);
m_volume = static_cast<float>(std::max(0.0, m_basevolume * get(Frame, port::volume)));
m_pitch = static_cast<float>(std::max(0.0, m_basepitch * get(Frame, port::pitch)));
}
char const *vehicle_sound::state_name(std::size_t const Index) const
{
switch (Index)
{
case 0:
return "playing";
case 1:
return "volume";
default:
return "pitch";
}
}
double vehicle_sound::state_value(std::size_t const Index) const
{
switch (Index)
{
case 0:
return m_playing ? 1.0 : 0.0;
case 1:
return m_volume;
default:
return m_pitch;
}
}
block_type vehicle_sound::describe()
{
block_type type;
type.id = "fb.sound";
type.category = "indicators";
type.name = "Sound";
type.description = "Sound of the vehicle or of its cab. Volume and pitch are the parameters multiplied by the inputs; looped sounds play while the play input is on.";
type.origin = block_origin::cab;
type.ports = {signal_in("play", value_type::boolean), signal_in("volume", value_type::real, 1.0, "", true), signal_in("pitch", value_type::real, 1.0, "", true)};
type.params = {
text_param("file"),
number_param("volume", 1.0),
number_param("pitch", 1.0),
boolean_param("loop", true),
choice_param("placement", {"general", "internal", "engine", "external", "external_ambient"}, "internal"),
number_param("range", -1.0, "m"),
number_param("offset_x", 0.0, "m"),
number_param("offset_y", 0.0, "m"),
number_param("offset_z", 0.0, "m"),
};
type.create = make_block<vehicle_sound>;
return type;
}
void register_cab_blocks(catalog &Catalog)
{
Catalog.add(cab_switch::describe());
Catalog.add(cab_lamp::describe());
Catalog.add(cab_gauge::describe(false));
Catalog.add(cab_gauge::describe(true));
Catalog.add(vehicle_sound::describe());
}
} // namespace vgraph

441
vehicle/graph/blocks/cab.h Normal file
View File

@@ -0,0 +1,441 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "vehicle/graph/graph_block.h"
#include <string>
#include <vector>
// cab controls and indicators. the blocks hold the logic and the values; the cab (TTrain) creates the animated
// submodels from their parameters and binds them to the values these blocks expose
namespace vgraph
{
// switch, button or lever of the cab. its behaviour and animation are defined in the graph, and its value
// is available to the other blocks
class cab_switch final : public block
{
public:
struct port
{
enum : std::size_t
{
value, // "pos": value of the current position, between min and max
index // number of the current position, from 0
};
};
struct param
{
enum : std::size_t
{
behaviour,
positions,
min,
max,
default_value,
spring,
labels,
animation,
step,
offset,
smoothing,
submodel,
key_inc,
key_dec,
command,
hw_name
};
};
enum class behaviour_type
{
bistable, // stays where it was put
monostable, // returns to the default position when released, e.g. a push button
multi // several positions; with the spring enabled it returns to the default position when released
};
enum class animation_type
{
rotate,
move
};
explicit cab_switch(block_setup const &Setup);
void update(frame &Frame) override;
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "position";
}
double state_value(std::size_t) const override
{
return m_position;
}
// operations of the player, AI or hardware. they take effect in the next update
void increase();
void decrease();
void toggle();
void press();
void release();
void set_position(int Position);
// moves to the position with the value closest to the specified one, e.g. for hardware levers
void set_value(double Value);
int position() const
{
return m_position;
}
int position_count() const
{
return m_positioncount;
}
int default_position() const
{
return m_defaultposition;
}
double value_at(int Position) const;
behaviour_type behaviour() const
{
return m_behaviour;
}
bool spring() const
{
return m_spring;
}
// description of the specified position, from the labels parameter; empty if not specified
std::string label(int Position) const;
// animation of the submodel: offset + step * position, in degrees or millimetres
animation_type animation() const
{
return m_animation;
}
double step() const
{
return m_step;
}
double offset() const
{
return m_offset;
}
double smoothing() const
{
return m_smoothing;
}
std::string const &submodel() const
{
return m_submodel;
}
// bindings: keys, existing simulator command, name for the hardware desk
std::string const &key_increase() const
{
return m_keyincrease;
}
std::string const &key_decrease() const
{
return m_keydecrease;
}
std::string const &command() const
{
return m_command;
}
std::string const &hardware_name() const
{
return m_hardwarename;
}
static block_type describe();
private:
int clamp_position(int Position) const;
behaviour_type const m_behaviour;
int const m_positioncount;
double const m_min;
double const m_max;
int m_defaultposition{0};
bool const m_spring;
std::vector<std::string> m_labels;
animation_type const m_animation;
double const m_step;
double const m_offset;
double const m_smoothing;
std::string const m_submodel;
std::string const m_keyincrease;
std::string const m_keydecrease;
std::string const m_command;
std::string const m_hardwarename;
int m_position{0};
};
// indicator lamp. when the graph simulates low voltage circuits and the supply input is connected, the lamp lights
// only with enough voltage, and draws current from the supply
class cab_lamp final : public block
{
public:
struct port
{
enum : std::size_t
{
on,
supply
};
};
struct param
{
enum : std::size_t
{
color,
blink,
blink_period,
min_voltage,
power,
submodel
};
};
explicit cab_lamp(block_setup const &Setup);
void update(frame &Frame) override;
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "lit";
}
double state_value(std::size_t) const override
{
return m_lit ? 1.0 : 0.0;
}
// stable address of the state, for TButton::AssignBool
bool const *lit_state() const
{
return &m_lit;
}
std::string const &submodel() const
{
return m_submodel;
}
static block_type describe();
private:
bool const m_supplied; // whether the supply input is connected
bool const m_blink;
double const m_blinkperiod;
double const m_minvoltage;
double const m_power;
std::string const m_submodel;
bool m_lit{false};
};
// needle gauge with one or two needles: voltmeter, ammeter, speedometer, pressure gauge
class cab_gauge final : public block
{
public:
struct port
{
enum : std::size_t
{
value,
value2
};
};
struct param
{
enum : std::size_t
{
min,
max,
unit,
angle_min,
angle_max,
smoothing,
submodel,
submodel2
};
};
cab_gauge(block_setup const &Setup, bool Twoneedles);
void update(frame &Frame) override;
std::size_t state_count() const override
{
return m_twoneedles ? 2 : 1;
}
char const *state_name(std::size_t Index) const override
{
return Index == 0 ? "value" : "value2";
}
double state_value(std::size_t Index) const override
{
return m_values[Index];
}
// stable addresses of the values shown by the needles, clamped to the scale, for TGauge::AssignFloat
float *value_state(std::size_t Needle)
{
return &m_values[Needle];
}
bool two_needles() const
{
return m_twoneedles;
}
double min() const
{
return m_min;
}
double max() const
{
return m_max;
}
double angle_min() const
{
return m_anglemin;
}
double angle_max() const
{
return m_anglemax;
}
double smoothing() const
{
return m_smoothing;
}
std::string const &submodel(std::size_t Needle) const
{
return Needle == 0 ? m_submodel : m_submodel2;
}
static block_type describe(bool Twoneedles);
private:
bool const m_twoneedles;
double const m_min;
double const m_max;
double const m_anglemin;
double const m_anglemax;
double const m_smoothing;
std::string const m_submodel;
std::string const m_submodel2;
float m_values[2]{0.f, 0.f};
};
// sound of the vehicle or of its cab. sounds of a graph vehicle are defined in the graph instead of the .mmd file
class vehicle_sound final : public block
{
public:
struct port
{
enum : std::size_t
{
play,
volume,
pitch
};
};
struct param
{
enum : std::size_t
{
file,
volume,
pitch,
loop,
placement,
range,
offset_x,
offset_y,
offset_z
};
};
// the same placements as sound_placement of the audio subsystem
enum class placement_type
{
general,
internal,
engine,
external,
external_ambient
};
explicit vehicle_sound(block_setup const &Setup);
void update(frame &Frame) override;
std::size_t state_count() const override
{
return 3;
}
char const *state_name(std::size_t Index) const override;
double state_value(std::size_t Index) const override;
bool playing() const
{
return m_playing;
}
// volume and pitch: parameter multiplied by the input
float volume() const
{
return m_volume;
}
float pitch() const
{
return m_pitch;
}
std::string const &file() const
{
return m_file;
}
bool loop() const
{
return m_loop;
}
placement_type placement() const
{
return m_placement;
}
double range() const
{
return m_range;
}
double offset_x() const
{
return m_offset[0];
}
double offset_y() const
{
return m_offset[1];
}
double offset_z() const
{
return m_offset[2];
}
static block_type describe();
private:
std::string const m_file;
double const m_basevolume;
double const m_basepitch;
bool const m_loop;
placement_type const m_placement;
double const m_range;
double const m_offset[3];
bool m_playing{false};
float m_volume{0.f};
float m_pitch{1.f};
};
} // namespace vgraph

View File

@@ -0,0 +1,625 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_catalog.h"
#include <algorithm>
#include <cmath>
// generic logic and arithmetic blocks. boolean signals are stored as 0 and 1, and read as true above 0.5
namespace vgraph
{
namespace
{
// tolerance of the equality comparisons; switch positions and constants are compared, not results of long computations
double constexpr equality_tolerance{1e-6};
block_type make_type(std::string Id, std::string Name, std::string Description)
{
block_type type;
type.id = std::move(Id);
type.category = "logic";
type.name = std::move(Name);
type.description = std::move(Description);
type.origin = block_origin::primitive;
return type;
}
class logic_compare final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
q
};
};
struct param
{
enum : std::size_t
{
op,
value
};
};
enum class operation
{
ne,
eq,
lt,
le,
gt,
ge
};
explicit logic_compare(block_setup const &Setup) : block(Setup), m_operation(static_cast<operation>(choice(Setup, param::op))), m_value(number(Setup, param::value)) {}
void update(frame &Frame) override
{
auto const a{get(Frame, port::a)};
auto result{false};
switch (m_operation)
{
case operation::ne:
result = std::abs(a - m_value) > equality_tolerance;
break;
case operation::eq:
result = std::abs(a - m_value) <= equality_tolerance;
break;
case operation::lt:
result = a < m_value;
break;
case operation::le:
result = a <= m_value + equality_tolerance;
break;
case operation::gt:
result = a > m_value;
break;
case operation::ge:
result = a >= m_value - equality_tolerance;
break;
}
set_bool(Frame, port::q, result);
}
static block_type describe()
{
auto type{make_type("logic.compare", "Compare", "Compares the input with a constant value.")};
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::boolean)};
type.params = {choice_param("op", {"ne", "eq", "lt", "le", "gt", "ge"}, "ne"), number_param("value", 0.0)};
type.create = make_block<logic_compare>;
return type;
}
private:
operation const m_operation;
double const m_value;
};
enum class gate
{
and_,
or_,
xor_,
nand_,
nor_
};
class logic_gate final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
b,
q
};
};
logic_gate(block_setup const &Setup, gate const Gate) : block(Setup), m_gate(Gate) {}
void update(frame &Frame) override
{
auto const a{get_bool(Frame, port::a)};
auto const b{get_bool(Frame, port::b)};
auto result{false};
switch (m_gate)
{
case gate::and_:
result = a && b;
break;
case gate::or_:
result = a || b;
break;
case gate::xor_:
result = a != b;
break;
case gate::nand_:
result = false == (a && b);
break;
case gate::nor_:
result = false == (a || b);
break;
}
set_bool(Frame, port::q, result);
}
template <gate Gate_> static std::unique_ptr<block> create(block_setup const &Setup)
{
return std::make_unique<logic_gate>(Setup, Gate_);
}
template <gate Gate_> static block_type describe(std::string Id, std::string Name, double const Default)
{
// unconnected inputs default to the neutral value of the gate, so a gate can be used with one input
auto type{make_type(std::move(Id), std::move(Name), "Logic gate.")};
type.ports = {signal_in("a", value_type::boolean, Default), signal_in("b", value_type::boolean, Default, "", true), signal_out("q", value_type::boolean)};
type.create = create<Gate_>;
return type;
}
private:
gate const m_gate;
};
class logic_not final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
q
};
};
explicit logic_not(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
set_bool(Frame, port::q, false == get_bool(Frame, port::a));
}
static block_type describe()
{
auto type{make_type("logic.not", "NOT", "Negation.")};
type.ports = {signal_in("a", value_type::boolean), signal_out("q", value_type::boolean)};
type.create = make_block<logic_not>;
return type;
}
};
// set-reset latch. it's deferred, so it can close loops, e.g. self-holding contactor circuits
class logic_sr final : public block
{
public:
struct port
{
enum : std::size_t
{
s,
r,
q
};
};
struct param
{
enum : std::size_t
{
reset_priority,
initial
};
};
explicit logic_sr(block_setup const &Setup) : block(Setup), m_resetpriority(boolean(Setup, param::reset_priority)), m_state(boolean(Setup, param::initial)) {}
void update(frame &Frame) override
{
auto const s{get_bool(Frame, port::s)};
auto const r{get_bool(Frame, port::r)};
if (s && r)
{
m_state = (false == m_resetpriority);
}
else if (s)
{
m_state = true;
}
else if (r)
{
m_state = false;
}
}
void commit(frame &Frame) override
{
set_bool(Frame, port::q, m_state);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "state";
}
double state_value(std::size_t) const override
{
return m_state ? 1.0 : 0.0;
}
static block_type describe()
{
auto type{make_type("logic.sr", "SR latch", "Set sets the output, reset clears it. The output changes in the next step, so the block can close loops.")};
type.deferred = true;
type.ports = {signal_in("s", value_type::boolean), signal_in("r", value_type::boolean, 0.0, "", true), signal_out("q", value_type::boolean)};
type.params = {boolean_param("reset_priority", true), boolean_param("initial", false)};
type.create = make_block<logic_sr>;
return type;
}
private:
bool const m_resetpriority;
bool m_state;
};
// on-delay (TON) and off-delay (TOF) timers
class logic_timer final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
q
};
};
struct param
{
enum : std::size_t
{
time
};
};
logic_timer(block_setup const &Setup, bool const Ondelay) : block(Setup), m_ondelay(Ondelay), m_delay(std::max(0.0, number(Setup, param::time))) {}
void update(frame &Frame) override
{
auto const a{get_bool(Frame, port::a)};
if (a == m_ondelay)
{
m_elapsed = std::min(m_elapsed + Frame.dt, m_delay);
}
else
{
m_elapsed = 0.0;
}
auto const expired{m_elapsed >= m_delay};
m_output = (m_ondelay ? (a && expired) : (a || false == expired));
}
void commit(frame &Frame) override
{
set_bool(Frame, port::q, m_output);
}
std::size_t state_count() const override
{
return 1;
}
char const *state_name(std::size_t) const override
{
return "elapsed";
}
double state_value(std::size_t) const override
{
return m_elapsed;
}
template <bool Ondelay_> static std::unique_ptr<block> create(block_setup const &Setup)
{
return std::make_unique<logic_timer>(Setup, Ondelay_);
}
template <bool Ondelay_> static block_type describe()
{
auto type{Ondelay_ ? make_type("logic.ton", "On delay", "The output follows the input switching on after the specified time, and switching off immediately.")
: make_type("logic.tof", "Off delay", "The output follows the input switching on immediately, and switching off after the specified time.")};
type.deferred = true;
type.ports = {signal_in("a", value_type::boolean), signal_out("q", value_type::boolean)};
type.params = {number_param("time", 1.0, "s")};
type.create = create<Ondelay_>;
return type;
}
private:
bool const m_ondelay;
double const m_delay;
// for the on-delay: time the input is on; for the off-delay: time the input is off
// (both start expired for the off-delay, so the output is off until the input is switched on)
double m_elapsed{m_ondelay ? 0.0 : m_delay};
bool m_output{false};
};
// delays the signal by one step; the explicit way to close a loop
class logic_delay final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
q
};
};
struct param
{
enum : std::size_t
{
initial
};
};
explicit logic_delay(block_setup const &Setup) : block(Setup), m_value(number(Setup, param::initial)) {}
void update(frame &Frame) override
{
m_value = get(Frame, port::a);
}
void commit(frame &Frame) override
{
set(Frame, port::q, m_value);
}
static block_type describe()
{
auto type{make_type("logic.delay", "Unit delay", "Passes the input to the output one step later. Use it to close loops.")};
type.deferred = true;
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::real)};
type.params = {number_param("initial", 0.0)};
type.create = make_block<logic_delay>;
return type;
}
private:
double m_value;
};
class logic_hysteresis final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
q
};
};
struct param
{
enum : std::size_t
{
lo,
hi,
initial
};
};
explicit logic_hysteresis(block_setup const &Setup)
: block(Setup), m_low(number(Setup, param::lo)), m_high(number(Setup, param::hi)), m_state(boolean(Setup, param::initial))
{
}
void update(frame &Frame) override
{
auto const a{get(Frame, port::a)};
if (a >= m_high)
{
m_state = true;
}
else if (a <= m_low)
{
m_state = false;
}
set_bool(Frame, port::q, m_state);
}
static block_type describe()
{
auto type{make_type("logic.hyst", "Hysteresis", "Switches on when the input reaches the upper threshold, and off when it drops to the lower one.")};
type.ports = {signal_in("a", value_type::real), signal_out("q", value_type::boolean)};
type.params = {number_param("lo", 0.0), number_param("hi", 1.0), boolean_param("initial", false)};
type.create = make_block<logic_hysteresis>;
return type;
}
private:
double const m_low;
double const m_high;
bool m_state;
};
class logic_const final : public block
{
public:
struct port
{
enum : std::size_t
{
q
};
};
struct param
{
enum : std::size_t
{
value
};
};
explicit logic_const(block_setup const &Setup) : block(Setup), m_value(number(Setup, param::value)) {}
void update(frame &Frame) override
{
set(Frame, port::q, m_value);
}
static block_type describe()
{
auto type{make_type("logic.const", "Constant", "Constant value.")};
type.ports = {signal_out("q", value_type::real)};
type.params = {number_param("value", 1.0)};
type.create = make_block<logic_const>;
return type;
}
private:
double const m_value;
};
class logic_math final : public block
{
public:
struct port
{
enum : std::size_t
{
a,
b,
q
};
};
struct param
{
enum : std::size_t
{
op
};
};
enum class operation
{
add,
sub,
mul,
div,
min,
max
};
explicit logic_math(block_setup const &Setup) : block(Setup), m_operation(static_cast<operation>(choice(Setup, param::op))) {}
void update(frame &Frame) override
{
auto const a{get(Frame, port::a)};
auto const b{get(Frame, port::b)};
auto result{0.0};
switch (m_operation)
{
case operation::add:
result = a + b;
break;
case operation::sub:
result = a - b;
break;
case operation::mul:
result = a * b;
break;
case operation::div:
// division by zero gives zero rather than infinity, which would spread through the whole graph
result = (b != 0.0 ? a / b : 0.0);
break;
case operation::min:
result = std::min(a, b);
break;
case operation::max:
result = std::max(a, b);
break;
}
set(Frame, port::q, result);
}
static block_type describe()
{
auto type{make_type("logic.math", "Arithmetic", "Arithmetic operation on two inputs. Division by zero gives zero.")};
type.ports = {signal_in("a", value_type::real), signal_in("b", value_type::real, 1.0), signal_out("q", value_type::real)};
type.params = {choice_param("op", {"add", "sub", "mul", "div", "min", "max"}, "mul")};
type.create = make_block<logic_math>;
return type;
}
private:
operation const m_operation;
};
class logic_select final : public block
{
public:
struct port
{
enum : std::size_t
{
sel,
a,
b,
q
};
};
explicit logic_select(block_setup const &Setup) : block(Setup) {}
void update(frame &Frame) override
{
set(Frame, port::q, get_bool(Frame, port::sel) ? get(Frame, port::b) : get(Frame, port::a));
}
static block_type describe()
{
auto type{make_type("logic.select", "Select", "Passes input a when sel is off, and input b when sel is on.")};
type.ports = {signal_in("sel", value_type::boolean), signal_in("a", value_type::real), signal_in("b", value_type::real), signal_out("q", value_type::real)};
type.create = make_block<logic_select>;
return type;
}
};
} // namespace
void register_logic_blocks(catalog &Catalog)
{
Catalog.add(logic_compare::describe());
Catalog.add(logic_gate::describe<gate::and_>("logic.and", "AND", 1.0));
Catalog.add(logic_gate::describe<gate::or_>("logic.or", "OR", 0.0));
Catalog.add(logic_gate::describe<gate::xor_>("logic.xor", "XOR", 0.0));
Catalog.add(logic_gate::describe<gate::nand_>("logic.nand", "NAND", 1.0));
Catalog.add(logic_gate::describe<gate::nor_>("logic.nor", "NOR", 0.0));
Catalog.add(logic_not::describe());
Catalog.add(logic_sr::describe());
Catalog.add(logic_timer::describe<true>());
Catalog.add(logic_timer::describe<false>());
Catalog.add(logic_delay::describe());
Catalog.add(logic_hysteresis::describe());
Catalog.add(logic_const::describe());
Catalog.add(logic_math::describe());
Catalog.add(logic_select::describe());
}
} // namespace vgraph