17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-02 12:49:13 +01:00

feat(editor): more models selected at once (Shift+LMB), moved and turned by the gizmo together, undone in one step; Ctrl+C / Ctrl+V copy the selected models with their groups and paste them at the cursor; Ctrl+G groups the selection, Ctrl+Shift+G takes a group apart, groups written to the scenery as group ... endgroup and loaded as groups of their own; End drops each model of the selection and of their groups onto the ground; menu of the selection at the cursor on Ctrl+RMB; Edit menu with undo, redo, the clipboard, grouping, deleting; Help > Keyboard shortcuts (F1) with the keys of every field of work, F2-F5 choose the fields of work, the tracks menu lists the track tools with their keys, the keys and descriptions in the menus and tooltips; self-test commands for the selection; Polish translation

This commit is contained in:
2026-10-09 01:12:46 +02:00
parent 791d5d052c
commit f94100ffe7
14 changed files with 1354 additions and 78 deletions

View File

@@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/.
#include "simulation/simulation.h"
#include "model/AnimModel.h"
#include "scene/scenenodegroups.h"
#include "world/MemCell.h"
#include "world/Traction.h"
#include "world/EvLaunch.h"
@@ -688,6 +689,11 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements, st
{
return true;
}
if (Model != nullptr && Groups.relocating(Node))
{
// written anew along with its group, below
return true;
}
auto const location{Node->location()};
auto const angles{Model != nullptr ? Model->Angles() : glm::vec3{0.f}};
auto const scale{Model != nullptr ? Model->Scale() : glm::vec3{1.f}};
@@ -806,6 +812,87 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements, st
return fail(state.error);
}
}
// models grouped or ungrouped in the editor: their definitions are taken out of where they are and written anew, a group
// as one block between group and endgroup in the layer of its first model, a model of a dissolved group on its own
std::set<basic_node const *> relocated;
auto const relocate = [&](TAnimModel const *Model, layer_handle const Target, std::vector<std::pair<basic_node const *, std::string>> &Output) {
if (auto const lookup{m_sources.find(Model)}; lookup != m_sources.end())
{
auto const &source{lookup->second};
if (false == writable(source.layer))
{
// one definition stands for more than one node, it stays where it is
return true;
}
if (false == load(source.layer))
{
return false;
}
file_patch::edit edit;
edit.begin = source.span.begin;
edit.end = source.span.end;
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(Model);
}
std::string text;
Model->export_as_text(text);
text.erase(text.find_last_not_of(" \t\r\n") + 1);
auto const &context{layer(Target).context_insert()};
if (false == context.matches(layer_context()))
{
model_placement placement;
placement.rotation = context.rotation;
placement.location = context.to_local(Model->location());
placement.angles = Model->Angles();
placement.scale = Model->Scale() / context.scale;
std::string error;
if (false == patch_model(text, placement, error))
{
state.error = error + " of \"" + Model->name() + "\"";
return false;
}
}
Output.emplace_back(Model, std::move(text));
relocated.emplace(Model);
return true;
};
for (auto *node : Groups.loose())
{
auto const *model{dynamic_cast<TAnimModel const *>(node)};
if (model == nullptr || model->from_template() || false == is_output(resolve(node->layer())))
{
continue;
}
if (false == relocate(model, resolve(node->layer()), created[resolve(node->layer())]))
{
return fail(state.error);
}
}
for (auto const group : Groups.rewritten())
{
auto const &nodes{Groups.group(group).nodes};
if (nodes.empty() || false == is_output(resolve(nodes.front()->layer())))
{
continue;
}
auto const target{resolve(nodes.front()->layer())};
auto &output{created[target]};
// NOTE: lines with no node are written as they are, without a place in the bookkeeping
output.emplace_back(nullptr, "group");
for (auto const *node : nodes)
{
auto const *model{dynamic_cast<TAnimModel const *>(node)};
if (model == nullptr || model->from_template())
{
continue;
}
if (false == relocate(model, target, output))
{
return fail(state.error);
}
}
output.emplace_back(nullptr, "endgroup");
}
auto const push_edit = [&](layer_handle const Layer, source_span const &Span, std::string Text, basic_node const *Node) {
file_patch::edit edit;
edit.begin = Span.begin;
@@ -1448,7 +1535,10 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements, st
{
ensure_newline(added.text, eol);
auto const offset{static_cast<std::streamoff>(added.text.size())};
added.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
if (definition.first != nullptr)
{
added.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
}
added.text += definition.second + eol;
}
}
@@ -1620,6 +1710,7 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements, st
}
// the scenery files now match the scene, update the bookkeeping to reflect it
Groups.saved();
auto removedterrain{false};
for (auto const output : outputs)
{
@@ -1629,7 +1720,8 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements, st
auto const isnew{source.layer == null_handle};
source.layer = output;
source.span = place.span;
if (isnew)
// a definition written anew elsewhere is in the placement of the place it went to, as a new one is
if (isnew || relocated.count(place.node) != 0)
{
source.context = layer(output).context_insert();
}

View File

@@ -22,9 +22,20 @@ node_groups Groups;
// requests creation of a new node group. returns: handle to the group
scene::group_handle
node_groups::create() {
node_groups::create( bool const Separate ) {
m_activegroup.push( create_handle() );
if( false == Separate ) {
m_activegroup.push( create_handle() );
return handle();
}
// a handle no other group has, see make()
auto group { std::max<group_handle>( 2, static_cast<group_handle>( m_groupmap.size() + 1 ) ) };
while( m_groupmap.find( group ) != m_groupmap.end() ) {
++group;
}
// NOTE: registered before the nodes come, so that a group nested in this one doesn't get the same handle
m_groupmap[ group ];
m_activegroup.push( group );
return handle();
}
@@ -334,6 +345,87 @@ node_groups::erase( group_map::const_iterator Group ) {
m_groupmap.erase( Group );
}
group_handle
node_groups::make( std::vector<scene::basic_node *> const &Nodes ) {
// a handle no other group has, past the first one, which the editor treats as no group at all
auto handle { std::max<group_handle>( 2, static_cast<group_handle>( m_groupmap.size() + 1 ) ) };
while( m_groupmap.find( handle ) != m_groupmap.end() ) {
++handle;
}
for( auto *node : Nodes ) {
leave( node );
m_loose.erase( node );
insert( handle, node );
}
m_rewrite.insert( handle );
return handle;
}
void
node_groups::dissolve( scene::group_handle const Group ) {
auto const lookup { m_groupmap.find( Group ) };
if( lookup == m_groupmap.end() ) { return; }
for( auto *node : lookup->second.nodes ) {
m_loose.insert( node );
}
m_rewrite.erase( Group );
erase( lookup );
}
void
node_groups::remove( scene::basic_node *Node ) {
leave( Node );
m_loose.erase( Node );
}
void
node_groups::leave( scene::basic_node *Node ) {
auto const group { Node->group() };
Node->group( null_handle );
auto const lookup { m_groupmap.find( group ) };
if( lookup == m_groupmap.end() ) { return; }
auto &nodes { lookup->second.nodes };
nodes.erase( std::remove( std::begin( nodes ), std::end( nodes ), Node ), std::end( nodes ) );
if( nodes.empty() && lookup->second.events.empty() ) {
// a group made in the editor goes with its last node
m_rewrite.erase( group );
m_groupmap.erase( lookup );
}
}
bool
node_groups::relocating( scene::basic_node const *Node ) const {
return ( m_loose.count( Node ) != 0 ) || ( m_rewrite.count( Node->group() ) != 0 );
}
std::vector<scene::group_handle>
node_groups::rewritten() const {
return { std::begin( m_rewrite ), std::end( m_rewrite ) };
}
std::vector<scene::basic_node *>
node_groups::loose() const {
std::vector<scene::basic_node *> nodes;
for( auto const *node : m_loose ) {
nodes.emplace_back( const_cast<scene::basic_node *>( node ) );
}
return nodes;
}
void
node_groups::saved() {
m_rewrite.clear();
m_loose.clear();
}
// creates handle for a new group
group_handle
node_groups::create_handle() {

View File

@@ -29,8 +29,9 @@ public:
node_groups() = default;
// methods
// requests creation of a new node group. returns: handle to the group
// nested groups are flattened into the outer one, unless Separate: then the group stands by itself (group ... endgroup)
group_handle
create();
create( bool const Separate = false );
// indicates creation of current group ended. returns: handle to the parent group or null_handle if group stack is empty
group_handle
close();
@@ -50,6 +51,26 @@ public:
scene::basic_group &
group( scene::group_handle const Group ) {
return m_groupmap[ Group ]; }
// groups made in the scenery editor. the nodes leave the groups they were in. returns: handle to the new group
group_handle
make( std::vector<scene::basic_node *> const &Nodes );
// the nodes of the group are left on their own
void
dissolve( scene::group_handle const Group );
// the node, about to be deleted, leaves its group
void
remove( scene::basic_node *Node );
// groups made or dissolved in the editor are written anew when the scenery is saved: the definitions of their nodes are
// taken out of where they are, a group's written as one block between group and endgroup, a dissolved one's nodes each on its own
bool
relocating( scene::basic_node const *Node ) const;
std::vector<scene::group_handle>
rewritten() const;
std::vector<scene::basic_node *>
loose() const;
// the scenery files match the groups now
void
saved();
// sends basic content of the class in legacy (text) format to provided stream
void
export_as_text( std::ostream &Output, bool const Dirty ) const;
@@ -66,9 +87,14 @@ private:
create_handle();
bool
assign_cross_switch(map::track_switch&sw, std::string &sw_name, const std::string &id, size_t idx);
// the node leaves the group it's in
void
leave( scene::basic_node *Node );
// members
group_map m_groupmap; // map of established node groups
std::stack<scene::group_handle> m_activegroup; // helper, group to be assigned to newly created nodes
std::set<scene::group_handle> m_rewrite; // groups made in the editor, to be written as blocks on save
std::set<scene::basic_node const *> m_loose; // nodes of groups dissolved in the editor, to be written on their own on save
};
extern node_groups Groups;