mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 05:09:20 +02:00
feat(editor): layers window, scenery save and include descriptions
This commit is contained in:
@@ -168,6 +168,7 @@ set(SOURCES
|
|||||||
"input/editorkeyboardinput.cpp"
|
"input/editorkeyboardinput.cpp"
|
||||||
"editor/editorSettings.cpp"
|
"editor/editorSettings.cpp"
|
||||||
"editor/editorModelSets.cpp"
|
"editor/editorModelSets.cpp"
|
||||||
|
"editor/editorIncludeInfo.cpp"
|
||||||
"editor/editorTerrain.cpp"
|
"editor/editorTerrain.cpp"
|
||||||
"editor/editorTerrainStreamer.cpp"
|
"editor/editorTerrainStreamer.cpp"
|
||||||
"editor/editorOrthophoto.cpp"
|
"editor/editorOrthophoto.cpp"
|
||||||
@@ -179,6 +180,7 @@ set(SOURCES
|
|||||||
"application/scenarioloaderuilayer.cpp"
|
"application/scenarioloaderuilayer.cpp"
|
||||||
"scene/scenenodegroups.cpp"
|
"scene/scenenodegroups.cpp"
|
||||||
"scene/scenelayers.cpp"
|
"scene/scenelayers.cpp"
|
||||||
|
"scene/scenelayerssave.cpp"
|
||||||
"simulation/simulationenvironment.cpp"
|
"simulation/simulationenvironment.cpp"
|
||||||
"simulation/simulationstateserializer.cpp"
|
"simulation/simulationstateserializer.cpp"
|
||||||
"rendering/precipitation.cpp"
|
"rendering/precipitation.cpp"
|
||||||
@@ -608,6 +610,13 @@ if (WITH_BETTER_RENDERER)
|
|||||||
target_link_libraries(${PROJECT_NAME} "betterRenderer")
|
target_link_libraries(${PROJECT_NAME} "betterRenderer")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# yaml-cpp, for the descriptions of scenery templates edited in the scenery editor.
|
||||||
|
# the experimental renderer brings in the same copy on its own, when enabled
|
||||||
|
if (NOT TARGET yaml-cpp)
|
||||||
|
add_subdirectory("betterRenderer/thirdparty/yaml-cpp" "${CMAKE_CURRENT_BINARY_DIR}/yaml-cpp" EXCLUDE_FROM_ALL)
|
||||||
|
endif ()
|
||||||
|
target_link_libraries(${PROJECT_NAME} yaml-cpp)
|
||||||
|
|
||||||
|
|
||||||
# Enable compiling with debug symbols
|
# Enable compiling with debug symbols
|
||||||
if(GENERATE_PDB)
|
if(GENERATE_PDB)
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ editor_mode::editor_mode() {
|
|||||||
// the area fill settings live in the node bank window, in the tab of the fill mode
|
// the area fill settings live in the node bank window, in the tab of the fill mode
|
||||||
ui()->set_fill_options([this]() { render_area_fill(); });
|
ui()->set_fill_options([this]() { render_area_fill(); });
|
||||||
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
|
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
|
||||||
|
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
|
||||||
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
|
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
|
||||||
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
|
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
|
||||||
gather_ground_triangles(Min, Max, true, 50.0f, Out);
|
gather_ground_triangles(Min, Max, true, 50.0f, Out);
|
||||||
@@ -606,6 +607,7 @@ void editor_mode::push_snapshot(scene::basic_node *node, EditorSnapshot::Action
|
|||||||
snap.node_name = node->name();
|
snap.node_name = node->name();
|
||||||
snap.position = node->location();
|
snap.position = node->location();
|
||||||
snap.uuid = node->uuid;
|
snap.uuid = node->uuid;
|
||||||
|
snap.layer = node->layer();
|
||||||
|
|
||||||
if (auto *model = dynamic_cast<TAnimModel *>(node))
|
if (auto *model = dynamic_cast<TAnimModel *>(node))
|
||||||
{
|
{
|
||||||
@@ -679,11 +681,13 @@ void editor_mode::undo_last()
|
|||||||
redoSnap.serialized = snap.serialized;
|
redoSnap.serialized = snap.serialized;
|
||||||
redoSnap.position = snap.position;
|
redoSnap.position = snap.position;
|
||||||
redoSnap.node_ptr = nullptr;
|
redoSnap.node_ptr = nullptr;
|
||||||
|
redoSnap.layer = snap.layer;
|
||||||
g_redo.push_back(std::move(redoSnap));
|
g_redo.push_back(std::move(redoSnap));
|
||||||
|
|
||||||
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
|
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
|
||||||
if (created)
|
if (created)
|
||||||
{
|
{
|
||||||
|
scene::Layers.move(created, snap.layer); // back to the layer it was deleted from, rather than the active one
|
||||||
created->location(snap.position);
|
created->location(snap.position);
|
||||||
created->Angles(snap.rotation);
|
created->Angles(snap.rotation);
|
||||||
m_node = created;
|
m_node = created;
|
||||||
@@ -702,6 +706,7 @@ void editor_mode::undo_last()
|
|||||||
current.action = snap.action;
|
current.action = snap.action;
|
||||||
current.node_name = snap.node_name;
|
current.node_name = snap.node_name;
|
||||||
current.node_ptr = target;
|
current.node_ptr = target;
|
||||||
|
current.layer = target->layer();
|
||||||
current.position = target->location();
|
current.position = target->location();
|
||||||
if (auto *model = dynamic_cast<TAnimModel *>(target))
|
if (auto *model = dynamic_cast<TAnimModel *>(target))
|
||||||
{
|
{
|
||||||
@@ -758,6 +763,7 @@ void editor_mode::redo_last()
|
|||||||
hist.serialized = snap.serialized;
|
hist.serialized = snap.serialized;
|
||||||
hist.position = snap.position;
|
hist.position = snap.position;
|
||||||
hist.uuid = snap.uuid;
|
hist.uuid = snap.uuid;
|
||||||
|
hist.layer = snap.layer;
|
||||||
m_history.push_back(std::move(hist));
|
m_history.push_back(std::move(hist));
|
||||||
|
|
||||||
scene::basic_node *target = simulation::Instances.find(snap.node_name);
|
scene::basic_node *target = simulation::Instances.find(snap.node_name);
|
||||||
@@ -799,6 +805,7 @@ void editor_mode::redo_last()
|
|||||||
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
|
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
|
||||||
if (created)
|
if (created)
|
||||||
{
|
{
|
||||||
|
scene::Layers.move(created, snap.layer);
|
||||||
created->location(snap.position);
|
created->location(snap.position);
|
||||||
created->Angles(snap.rotation);
|
created->Angles(snap.rotation);
|
||||||
created->Scale(snap.scale);
|
created->Scale(snap.scale);
|
||||||
@@ -893,7 +900,7 @@ bool editor_mode::update()
|
|||||||
update_camera(deltarealtime);
|
update_camera(deltarealtime);
|
||||||
|
|
||||||
// drop the selection if its layer was hidden or locked in the meantime
|
// drop the selection if its layer was hidden or locked in the meantime
|
||||||
if (m_node != nullptr && false == scene::Layers.editable(m_node->layer()))
|
if (m_node != nullptr && false == scene::Layers.editable(m_node))
|
||||||
{
|
{
|
||||||
m_node = nullptr;
|
m_node = nullptr;
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
@@ -1497,6 +1504,15 @@ void editor_mode::create_chunked_terrain()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void editor_mode::save_scene_with_terrain()
|
void editor_mode::save_scene_with_terrain()
|
||||||
|
{
|
||||||
|
commit_terrain();
|
||||||
|
|
||||||
|
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
|
||||||
|
export_scenery();
|
||||||
|
WriteLog("Editor: saved scene + terrain", logtype::generic);
|
||||||
|
}
|
||||||
|
|
||||||
|
void editor_mode::commit_terrain()
|
||||||
{
|
{
|
||||||
// commit authored terrain so the scenery streams it on load. if not already streaming, hand the
|
// commit authored terrain so the scenery streams it on load. if not already streaming, hand the
|
||||||
// manual grid chunks over to the streamer (same as toggling Stream terrain on)
|
// manual grid chunks over to the streamer (same as toggling Stream terrain on)
|
||||||
@@ -1529,10 +1545,37 @@ void editor_mode::save_scene_with_terrain()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_streamer.flush(); // save resident edited chunks to disk
|
m_streamer.flush(); // save resident edited chunks to disk
|
||||||
|
}
|
||||||
|
|
||||||
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
|
void editor_mode::save()
|
||||||
export_scenery();
|
{
|
||||||
WriteLog("Editor: saved scene + terrain", logtype::generic);
|
if (scene::Layers.empty())
|
||||||
|
{
|
||||||
|
// without the sources tracked during the load there's no telling where the changes belong
|
||||||
|
ui()->set_status("The scenery wasn't opened for editing. Start the simulator with: -edit <scenery file>", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// terrain made in the editor is kept in files of its own, the scenery only needs the directive which loads it
|
||||||
|
std::vector<std::string> rootstatements;
|
||||||
|
if (m_streamer.active() || false == m_grid_chunks.empty())
|
||||||
|
{
|
||||||
|
commit_terrain();
|
||||||
|
if (false == scene::Layers.terrain_directive())
|
||||||
|
{
|
||||||
|
rootstatements.emplace_back(
|
||||||
|
"editorterrain " + m_streamer.directory() + ' ' + std::to_string(m_streamer.cells()) + ' ' + to_string(m_streamer.cellsize(), 3) + ' '
|
||||||
|
+ std::to_string(m_streamer.radius()) + " endeditorterrain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const result = scene::Layers.save(rootstatements);
|
||||||
|
if (result.success && false == rootstatements.empty())
|
||||||
|
{
|
||||||
|
scene::Layers.terrain_directive(true);
|
||||||
|
}
|
||||||
|
ui()->set_status(result.message, false == result.success);
|
||||||
|
WriteLog("Editor: " + result.message, logtype::generic);
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_mode::export_scenery()
|
void editor_mode::export_scenery()
|
||||||
@@ -2196,6 +2239,14 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save; checked ahead of the camera keys, which have S bound to moving back
|
||||||
|
if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S)
|
||||||
|
{
|
||||||
|
if (is_press(Action))
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// then internal input handling
|
// then internal input handling
|
||||||
if (m_input.keyboard.key(Key, Action))
|
if (m_input.keyboard.key(Key, Action))
|
||||||
return;
|
return;
|
||||||
@@ -2364,9 +2415,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
|||||||
|
|
||||||
m_node = nullptr;
|
m_node = nullptr;
|
||||||
|
|
||||||
// nodes of locked layers can't be selected
|
// in a scenery opened for editing some nodes are off limits: the ones in locked layers,
|
||||||
if (node && false == scene::Layers.editable(node->layer()))
|
// and the ones whose definitions can't be rewritten on save
|
||||||
|
if (std::string reason; node && false == scene::Layers.editable(node, &reason))
|
||||||
|
{
|
||||||
|
ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true);
|
||||||
node = nullptr;
|
node = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// ignore picks that are beyond allowed placement distance
|
// ignore picks that are beyond allowed placement distance
|
||||||
if (node) {
|
if (node) {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class editor_mode : public application_mode
|
|||||||
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
|
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
|
||||||
glm::vec3 scale{1.0f, 1.0f, 1.0f};
|
glm::vec3 scale{1.0f, 1.0f, 1.0f};
|
||||||
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
|
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
|
||||||
|
scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it
|
||||||
|
|
||||||
};
|
};
|
||||||
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
|
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
|
||||||
@@ -160,8 +161,12 @@ class editor_mode : public application_mode
|
|||||||
void remove_grid_chunk(int Cx, int Cz);
|
void remove_grid_chunk(int Cx, int Cz);
|
||||||
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
|
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
|
||||||
void handle_chunk_edit_click(bool DeleteMode);
|
void handle_chunk_edit_click(bool DeleteMode);
|
||||||
// commits authored terrain to disk, enables streaming, and exports the scenery (Ctrl+S)
|
// commits authored terrain to disk, enables streaming, and exports the scenery
|
||||||
void save_scene_with_terrain();
|
void save_scene_with_terrain();
|
||||||
|
// commits authored terrain to disk and enables streaming
|
||||||
|
void commit_terrain();
|
||||||
|
// writes the changes to the files of scenery opened for editing (Ctrl+S)
|
||||||
|
void save();
|
||||||
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
|
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
|
||||||
void export_scenery();
|
void export_scenery();
|
||||||
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode
|
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include "scene/scenenode.h"
|
#include "scene/scenenode.h"
|
||||||
#include "scene/scenelayers.h"
|
#include "scene/scenelayers.h"
|
||||||
#include "rendering/renderer.h"
|
#include "rendering/renderer.h"
|
||||||
|
#include "utilities/translation.h"
|
||||||
|
|
||||||
editor_ui::editor_ui()
|
editor_ui::editor_ui()
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,8 @@ editor_ui::editor_ui()
|
|||||||
// bind the panels with ui object. maybe not the best place for this but, eh
|
// bind the panels with ui object. maybe not the best place for this but, eh
|
||||||
|
|
||||||
add_external_panel(&m_nodebankpanel);
|
add_external_panel(&m_nodebankpanel);
|
||||||
|
add_external_panel(&m_layerspanel);
|
||||||
|
add_external_panel(&m_includespanel);
|
||||||
|
|
||||||
m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); };
|
m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); };
|
||||||
m_nodebankpanel.header_sections = [this]() { render_header_sections(); };
|
m_nodebankpanel.header_sections = [this]() { render_header_sections(); };
|
||||||
@@ -100,98 +103,55 @@ void editor_ui::render_header_sections()
|
|||||||
m_itempropertiespanel.render_body();
|
m_itempropertiespanel.render_body();
|
||||||
ImGui::Unindent();
|
ImGui::Unindent();
|
||||||
}
|
}
|
||||||
if (ImGui::CollapsingHeader("Layers"))
|
}
|
||||||
|
|
||||||
|
void editor_ui::render_menu_contents()
|
||||||
|
{
|
||||||
|
if (ImGui::BeginMenu(STR_C("File")))
|
||||||
{
|
{
|
||||||
ImGui::Indent();
|
// changes go to the scenery files only if the scenery was loaded with its sources tracked
|
||||||
render_layers();
|
auto const editsession{false == scene::Layers.empty()};
|
||||||
ImGui::Unindent();
|
if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save)
|
||||||
|
{
|
||||||
|
m_save();
|
||||||
|
}
|
||||||
|
if (false == editsession && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit <scenery file> to save changes to the scenery files"));
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(STR_C("Export scenery dump"), "Ctrl+Shift+F11") && m_export)
|
||||||
|
{
|
||||||
|
m_export();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", STR_C("Writes the whole scene to _export files next to the scenario, the scenery files are left as they are"));
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_layer::render_menu_contents();
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu(STR_C("Mode windows")))
|
||||||
|
{
|
||||||
|
ImGui::MenuItem(STR_C("Toolset"), nullptr, &m_nodebankpanel.is_open);
|
||||||
|
ImGui::MenuItem(STR_C("Layers"), nullptr, &m_layerspanel.is_open);
|
||||||
|
ImGui::MenuItem(STR_C("Include descriptions"), nullptr, &m_includespanel.is_open);
|
||||||
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_ui::render_layers()
|
void editor_ui::set_file_actions(std::function<void()> Save, std::function<void()> Export)
|
||||||
{
|
{
|
||||||
if (scene::Layers.empty())
|
m_save = Save;
|
||||||
{
|
m_export = std::move(Export);
|
||||||
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
|
m_layerspanel.save = std::move(Save);
|
||||||
ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>");
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TextDisabled("visible, locked, layer file (click: make active)");
|
void editor_ui::set_status(std::string const &Status, bool const Error)
|
||||||
|
{
|
||||||
std::pair<char const *, scene::layer_item> const itemlabels[] = {{"models", scene::layer_item::model},
|
m_layerspanel.status = Status;
|
||||||
{"tracks", scene::layer_item::track},
|
m_layerspanel.status_error = Error;
|
||||||
{"traction", scene::layer_item::traction},
|
|
||||||
{"power sources", scene::layer_item::powersource},
|
|
||||||
{"memory cells", scene::layer_item::memcell},
|
|
||||||
{"event launchers", scene::layer_item::launcher},
|
|
||||||
{"events", scene::layer_item::event},
|
|
||||||
{"vehicles", scene::layer_item::vehicle},
|
|
||||||
{"sounds", scene::layer_item::sound},
|
|
||||||
{"terrain shapes", scene::layer_item::shape},
|
|
||||||
{"lines", scene::layer_item::lines}};
|
|
||||||
|
|
||||||
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
|
||||||
{
|
|
||||||
auto const handle{static_cast<scene::layer_handle>(idx)};
|
|
||||||
auto const &layer{scene::Layers.layer(handle)};
|
|
||||||
auto const isactive{handle == scene::Layers.active()};
|
|
||||||
|
|
||||||
ImGui::PushID(static_cast<int>(idx));
|
|
||||||
// active layer receives new nodes, so it can't be hidden nor locked
|
|
||||||
if (isactive)
|
|
||||||
{
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
|
||||||
}
|
|
||||||
auto visible{layer.visible};
|
|
||||||
if (ImGui::Checkbox("##visible", &visible) && false == isactive)
|
|
||||||
{
|
|
||||||
scene::Layers.visible(handle, visible);
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer");
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
auto locked{layer.locked};
|
|
||||||
if (ImGui::Checkbox("##locked", &locked) && false == isactive)
|
|
||||||
{
|
|
||||||
scene::Layers.locked(handle, locked);
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport");
|
|
||||||
}
|
|
||||||
if (isactive)
|
|
||||||
{
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
// layers are listed in the order the files were first opened, so indentation alone shows the include tree
|
|
||||||
auto const label{std::string(2 * scene::Layers.depth(handle), ' ') + Bezogonkow(layer.name) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
|
|
||||||
if (ImGui::Selectable(label.c_str(), isactive))
|
|
||||||
{
|
|
||||||
scene::Layers.active(handle);
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
{
|
|
||||||
std::string content{isactive ? "Active layer, nodes created in the editor are placed here" : "Click to make this the active layer"};
|
|
||||||
if (layer.binary)
|
|
||||||
{
|
|
||||||
content += "\nThe file wasn't parsed, its content was loaded from binary terrain file";
|
|
||||||
}
|
|
||||||
for (auto const &itemlabel : itemlabels)
|
|
||||||
{
|
|
||||||
auto const count{layer.items[static_cast<std::size_t>(itemlabel.second)]};
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
content += "\n" + std::string{itemlabel.first} + ": " + std::to_string(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::SetTooltip("%s", content.c_str());
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_ui::render_rotation_controls()
|
void editor_ui::render_rotation_controls()
|
||||||
|
|||||||
@@ -53,17 +53,24 @@ class editor_ui : public ui_layer
|
|||||||
{
|
{
|
||||||
m_gizmooptions = std::move(Renderer);
|
m_gizmooptions = std::move(Renderer);
|
||||||
}
|
}
|
||||||
|
// actions of the file menu, performed by the editor mode
|
||||||
|
void set_file_actions(std::function<void()> Save, std::function<void()> Export);
|
||||||
|
// shows outcome of an operation in the layers window
|
||||||
|
void set_status(std::string const &Status, bool const Error = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// methods
|
// methods
|
||||||
void render_mode_options(nodebank_panel::edit_mode const Mode);
|
void render_mode_options(nodebank_panel::edit_mode const Mode);
|
||||||
void render_header_sections();
|
void render_header_sections();
|
||||||
// list of scenery layers: visibility, selection lock and choice of the layer receiving new nodes
|
void render_menu_contents() override;
|
||||||
void render_layers();
|
|
||||||
// members
|
// members
|
||||||
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
|
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
|
||||||
functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
|
functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
|
||||||
nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
|
nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
|
||||||
|
layers_panel m_layerspanel{"Layers", true};
|
||||||
|
includes_panel m_includespanel{"Include descriptions", false};
|
||||||
|
std::function<void()> m_save;
|
||||||
|
std::function<void()> m_export;
|
||||||
brush_object_list m_brushobjects;
|
brush_object_list m_brushobjects;
|
||||||
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
|
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
|
||||||
model_set_ref m_insertset;
|
model_set_ref m_insertset;
|
||||||
|
|||||||
@@ -1076,4 +1076,581 @@ void functions_panel::render_controls()
|
|||||||
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
|
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
|
||||||
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg");
|
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::pair<char const *, scene::layer_item> const layer_itemlabels[] = {{"models", scene::layer_item::model},
|
||||||
|
{"tracks", scene::layer_item::track},
|
||||||
|
{"traction", scene::layer_item::traction},
|
||||||
|
{"power sources", scene::layer_item::powersource},
|
||||||
|
{"memory cells", scene::layer_item::memcell},
|
||||||
|
{"event launchers", scene::layer_item::launcher},
|
||||||
|
{"events", scene::layer_item::event},
|
||||||
|
{"vehicles", scene::layer_item::vehicle},
|
||||||
|
{"sounds", scene::layer_item::sound},
|
||||||
|
{"terrain shapes", scene::layer_item::shape},
|
||||||
|
{"lines", scene::layer_item::lines}};
|
||||||
|
|
||||||
|
// what the layer holds, a line per kind of item
|
||||||
|
std::string layer_content(scene::basic_layer const &Layer)
|
||||||
|
{
|
||||||
|
std::string content;
|
||||||
|
for (auto const &itemlabel : layer_itemlabels)
|
||||||
|
{
|
||||||
|
auto const count{Layer.items[static_cast<std::size_t>(itemlabel.second)]};
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
content += (content.empty() ? "" : "\n") + std::string{itemlabel.first} + ": " + std::to_string(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string layer_name(scene::layer_handle const Layer)
|
||||||
|
{
|
||||||
|
return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : "(none)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// button which can be greyed out, with an explanation shown when it's hovered
|
||||||
|
bool action_button(char const *Label, bool const Enabled, std::string const &Reason = std::string())
|
||||||
|
{
|
||||||
|
if (false == Enabled)
|
||||||
|
{
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
||||||
|
}
|
||||||
|
auto const clicked{ImGui::Button(Label) && Enabled};
|
||||||
|
if (false == Enabled)
|
||||||
|
{
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
if (false == Reason.empty() && ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", Reason.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// text field bound to a string
|
||||||
|
bool input_text(char const *Label, std::string &Value)
|
||||||
|
{
|
||||||
|
std::array<char, 256> buffer{};
|
||||||
|
Value.copy(buffer.data(), buffer.size() - 1);
|
||||||
|
if (false == ImGui::InputText(Label, buffer.data(), buffer.size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Value = buffer.data();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void layers_panel::report(std::string const &Status, bool const Error)
|
||||||
|
{
|
||||||
|
status = Status;
|
||||||
|
status_error = Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void layers_panel::render()
|
||||||
|
{
|
||||||
|
if (false == is_open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initially next to the toolset window
|
||||||
|
ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver);
|
||||||
|
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
|
||||||
|
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
|
||||||
|
{
|
||||||
|
if (scene::Layers.empty())
|
||||||
|
{
|
||||||
|
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
|
||||||
|
ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (false == scene::Layers.listed(m_selected))
|
||||||
|
{
|
||||||
|
m_selected = null_handle;
|
||||||
|
}
|
||||||
|
auto const selected{m_selected != null_handle};
|
||||||
|
auto const removed{selected && scene::Layers.layer(m_selected).removed};
|
||||||
|
std::string reason{"Select a layer first"};
|
||||||
|
|
||||||
|
if (ImGui::Button("New..."))
|
||||||
|
{
|
||||||
|
m_newname[0] = '\0';
|
||||||
|
m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1});
|
||||||
|
m_popuperror.clear();
|
||||||
|
ImGui::OpenPopup("New layer");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
if (ImGui::Button("Restore"))
|
||||||
|
{
|
||||||
|
scene::Layers.restore(m_selected);
|
||||||
|
report("Layer \"" + layer_name(m_selected) + "\" restored");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (action_button("Remove...", selected && scene::Layers.can_remove(m_selected, reason), reason))
|
||||||
|
{
|
||||||
|
ImGui::OpenPopup("Remove layer");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
// the scenario file is what holds the scenery together, it stays where it is
|
||||||
|
auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())};
|
||||||
|
if (action_button("Merge into...", mergeable, selected && false == removed ? "The scenario file can't be merged into another layer" : "Select a layer first"))
|
||||||
|
{
|
||||||
|
// the layer which includes the selected one is the most likely target
|
||||||
|
m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent);
|
||||||
|
if (false == scene::Layers.can_merge(m_selected, m_mergetarget, reason))
|
||||||
|
{
|
||||||
|
m_mergetarget = null_handle;
|
||||||
|
}
|
||||||
|
ImGui::OpenPopup("Merge layer");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Save") && save)
|
||||||
|
{
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("Writes changed nodes and layers to the scenery files (Ctrl+S)");
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
render_list();
|
||||||
|
|
||||||
|
if (false == status.empty())
|
||||||
|
{
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
if (status_error)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(status_errorcolor, "%s", status.c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(status.c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_popups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void layers_panel::render_list()
|
||||||
|
{
|
||||||
|
// the list takes the window except for the room left for the status
|
||||||
|
ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f));
|
||||||
|
ImGui::TextDisabled("visible, locked, active, layer file");
|
||||||
|
|
||||||
|
for (auto const handle : scene::Layers.tree())
|
||||||
|
{
|
||||||
|
// NOTE: copies of the flags, as the calls below can change the layer
|
||||||
|
auto const &layer{scene::Layers.layer(handle)};
|
||||||
|
auto const isactive{handle == scene::Layers.active()};
|
||||||
|
auto const isremoved{layer.removed};
|
||||||
|
std::string readonly;
|
||||||
|
auto const iswritable{scene::Layers.writable(handle, &readonly)};
|
||||||
|
|
||||||
|
ImGui::PushID(static_cast<int>(handle));
|
||||||
|
if (isremoved)
|
||||||
|
{
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
||||||
|
}
|
||||||
|
// active layer receives new nodes, so it can't be hidden nor locked. removed layer is out of the picture altogether
|
||||||
|
auto const fixed{isactive || isremoved};
|
||||||
|
auto visible{layer.visible};
|
||||||
|
if (ImGui::Checkbox("##visible", &visible) && false == fixed)
|
||||||
|
{
|
||||||
|
scene::Layers.visible(handle, visible);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
auto locked{layer.locked};
|
||||||
|
if (ImGui::Checkbox("##locked", &locked) && false == fixed)
|
||||||
|
{
|
||||||
|
scene::Layers.locked(handle, locked);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::RadioButton("##active", isactive) && false == isactive)
|
||||||
|
{
|
||||||
|
if (isremoved || false == iswritable)
|
||||||
|
{
|
||||||
|
report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (isremoved ? "" : ": " + readonly), true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scene::Layers.active(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("Active layer: nodes created in the editor are placed in it");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
|
||||||
|
if (layer.created)
|
||||||
|
{
|
||||||
|
label += " [new]";
|
||||||
|
}
|
||||||
|
if (isremoved)
|
||||||
|
{
|
||||||
|
label += " [removed]";
|
||||||
|
}
|
||||||
|
else if (false == iswritable)
|
||||||
|
{
|
||||||
|
label += " [read-only]";
|
||||||
|
}
|
||||||
|
if (ImGui::Selectable(label.c_str(), handle == m_selected))
|
||||||
|
{
|
||||||
|
m_selected = handle;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
std::string content{layer.created ? "New layer, its file is created on save" : isremoved ? "Dropped from the scenery on save" : "Scenery file"};
|
||||||
|
if (scene::Layers.valid(layer.parent))
|
||||||
|
{
|
||||||
|
content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent));
|
||||||
|
}
|
||||||
|
if (false == iswritable)
|
||||||
|
{
|
||||||
|
content += "\nread-only: " + readonly;
|
||||||
|
}
|
||||||
|
if (auto const items{layer_content(layer)}; false == items.empty())
|
||||||
|
{
|
||||||
|
content += "\n" + items;
|
||||||
|
}
|
||||||
|
ImGui::SetTooltip("%s", content.c_str());
|
||||||
|
}
|
||||||
|
if (isremoved)
|
||||||
|
{
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void layers_panel::render_popups()
|
||||||
|
{
|
||||||
|
if (ImGui::BeginPopupModal("New layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("File of the new layer, in the scenery directory:");
|
||||||
|
ImGui::InputTextWithHint("##name", "name.scm", m_newname, IM_ARRAYSIZE(m_newname));
|
||||||
|
if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str()))
|
||||||
|
{
|
||||||
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
||||||
|
if (false == scene::Layers.listed(handle) || scene::Layers.layer(handle).removed || scene::Layers.layer(handle).binary)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_newparent))
|
||||||
|
{
|
||||||
|
m_newparent = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (false == m_popuperror.empty())
|
||||||
|
{
|
||||||
|
ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str());
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Create"))
|
||||||
|
{
|
||||||
|
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
|
||||||
|
if (created != null_handle)
|
||||||
|
{
|
||||||
|
m_selected = created;
|
||||||
|
report("Layer \"" + layer_name(created) + "\" created and made active. Its file is written on save.");
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel"))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal("Remove layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
if (false == scene::Layers.listed(m_selected))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto const &layer{scene::Layers.layer(m_selected)};
|
||||||
|
auto included{0};
|
||||||
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
||||||
|
if (handle != m_selected && scene::Layers.listed(handle) && scene::Layers.is_descendant(handle, m_selected))
|
||||||
|
{
|
||||||
|
++included;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Text("Remove layer \"%s\" from the scenery?", layer_name(m_selected).c_str());
|
||||||
|
ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk.");
|
||||||
|
if (included > 0)
|
||||||
|
{
|
||||||
|
ImGui::Text("%d layer(s) it includes are removed with it.", included);
|
||||||
|
}
|
||||||
|
if (auto const items{layer_content(layer)}; false == items.empty())
|
||||||
|
{
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("The layer holds:");
|
||||||
|
ImGui::TextUnformatted(items.c_str());
|
||||||
|
ImGui::TextDisabled("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.");
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Remove"))
|
||||||
|
{
|
||||||
|
auto const name{layer_name(m_selected)};
|
||||||
|
if (scene::Layers.remove(m_selected))
|
||||||
|
{
|
||||||
|
report("Layer \"" + name + "\" removed. It can be restored until the scenery is saved.");
|
||||||
|
}
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel"))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal("Merge layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
if (false == scene::Layers.listed(m_selected))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::Text("Move content of layer \"%s\" to:", layer_name(m_selected).c_str());
|
||||||
|
std::string reason;
|
||||||
|
auto targets{0};
|
||||||
|
if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str()))
|
||||||
|
{
|
||||||
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
||||||
|
if (false == scene::Layers.listed(handle) || false == scene::Layers.can_merge(m_selected, handle, reason))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++targets;
|
||||||
|
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_mergetarget))
|
||||||
|
{
|
||||||
|
m_mergetarget = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targets == 0)
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("no layer can take it");
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
ImGui::TextDisabled("The text of the file is moved on save, the emptied file stays on the disk.\n"
|
||||||
|
"Merged into the layer which includes it, the content stays where the include was.\n"
|
||||||
|
"Merged into any other layer it's placed at its end, which changes the load order.");
|
||||||
|
auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)};
|
||||||
|
if (action_button("Merge", possible, m_mergetarget != null_handle ? reason : "Pick the target layer"))
|
||||||
|
{
|
||||||
|
auto const name{layer_name(m_selected)};
|
||||||
|
auto const target{m_mergetarget};
|
||||||
|
if (scene::Layers.merge(m_selected, target))
|
||||||
|
{
|
||||||
|
report("Layer \"" + name + "\" merged into \"" + layer_name(target) + "\". The files are changed on save.");
|
||||||
|
m_selected = target;
|
||||||
|
}
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel"))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void includes_panel::open(std::string const &File)
|
||||||
|
{
|
||||||
|
m_file = File;
|
||||||
|
m_statuserror = false;
|
||||||
|
std::string error;
|
||||||
|
m_loaded = editor_includes::load(File, m_info, error);
|
||||||
|
if (false == m_loaded)
|
||||||
|
{
|
||||||
|
m_status = error;
|
||||||
|
m_statuserror = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// every parameter the template uses gets an entry, described or not
|
||||||
|
auto const count{editor_includes::parameter_count(File)};
|
||||||
|
for (auto id = 1; id <= count; ++id)
|
||||||
|
{
|
||||||
|
m_info.parameter(id);
|
||||||
|
}
|
||||||
|
m_status = "The template uses " + std::to_string(count) + " parameter(s)";
|
||||||
|
}
|
||||||
|
|
||||||
|
void includes_panel::render()
|
||||||
|
{
|
||||||
|
if (false == is_open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2S(560, 110), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2S(520, 440), ImGuiCond_FirstUseEver);
|
||||||
|
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
|
||||||
|
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
|
||||||
|
{
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
||||||
|
ImGui::TextWrapped("Describes .inc templates and their parameters. The description is stored in the template file, as //$e comment lines.");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
// templates the loaded scenery makes use of; known only for scenery opened for editing
|
||||||
|
auto const &templates{scene::Layers.templates()};
|
||||||
|
if (false == templates.empty())
|
||||||
|
{
|
||||||
|
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 6.0f), true);
|
||||||
|
for (auto const &file : templates)
|
||||||
|
{
|
||||||
|
if (ImGui::Selectable(Bezogonkow(file).c_str(), file == m_file))
|
||||||
|
{
|
||||||
|
open(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
ImGui::PushItemWidth(-ImGui::CalcTextSize(" Open ").x - ImGui::GetStyle().ItemSpacing.x * 2.0f);
|
||||||
|
auto const entered{ImGui::InputTextWithHint("##file", "path of an .inc file in the scenery directory", m_filename, IM_ARRAYSIZE(m_filename), ImGuiInputTextFlags_EnterReturnsTrue)};
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if ((ImGui::Button("Open") || entered) && m_filename[0] != '\0')
|
||||||
|
{
|
||||||
|
std::string file{ToLower(m_filename)};
|
||||||
|
replace_slashes(file);
|
||||||
|
open(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_loaded)
|
||||||
|
{
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("%s", Bezogonkow(m_file).c_str());
|
||||||
|
input_text("Name", m_info.name);
|
||||||
|
input_text("Category", m_info.category);
|
||||||
|
input_text("Description", m_info.description);
|
||||||
|
|
||||||
|
ImGui::Columns(4, "##parameters", false);
|
||||||
|
ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x);
|
||||||
|
ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f);
|
||||||
|
ImGui::TextDisabled("param");
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::TextDisabled("role");
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::TextDisabled("label");
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::TextDisabled("default");
|
||||||
|
ImGui::NextColumn();
|
||||||
|
for (auto ¶meter : m_info.parameters)
|
||||||
|
{
|
||||||
|
ImGui::PushID(parameter.id);
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::Text("(p%d)", parameter.id);
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::PushItemWidth(-1.0f);
|
||||||
|
if (ImGui::BeginCombo("##role", parameter.role.c_str()))
|
||||||
|
{
|
||||||
|
for (auto const &role : editor_includes::roles)
|
||||||
|
{
|
||||||
|
if (ImGui::Selectable(role.c_str(), role == parameter.role))
|
||||||
|
{
|
||||||
|
parameter.role = role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::PushItemWidth(-1.0f);
|
||||||
|
input_text("##label", parameter.label);
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::PushItemWidth(-1.0f);
|
||||||
|
input_text("##default", parameter.value);
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::Columns(1);
|
||||||
|
|
||||||
|
if (ImGui::Button("Suggest roles"))
|
||||||
|
{
|
||||||
|
editor_includes::suggest(m_file, m_info);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Add parameter"))
|
||||||
|
{
|
||||||
|
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Save to file"))
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
m_statuserror = false == editor_includes::save(m_file, m_info, error);
|
||||||
|
m_status = m_statuserror ? error : "Description saved in \"" + m_file + "\"";
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reload"))
|
||||||
|
{
|
||||||
|
open(m_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (false == m_status.empty())
|
||||||
|
{
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
if (m_statuserror)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(m_status).c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted(Bezogonkow(m_status).c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
|
|
||||||
#include "application/uilayer.h"
|
#include "application/uilayer.h"
|
||||||
#include "utilities/Classes.h"
|
#include "utilities/Classes.h"
|
||||||
|
#include "editor/editorIncludeInfo.hpp"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
/*
|
/*
|
||||||
@@ -148,6 +149,56 @@ class nodebank_panel : public ui_panel
|
|||||||
model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set
|
model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// scenery layers of a scenery opened for editing: their visibility, selection lock, the layer receiving new nodes,
|
||||||
|
// and layer management. the changes are applied to the scenery files on save
|
||||||
|
class layers_panel : public ui_panel
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
layers_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
|
||||||
|
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
// saves the scenery; provided by the editor mode
|
||||||
|
std::function<void()> save;
|
||||||
|
// outcome of the most recent operation, shown at the bottom of the window
|
||||||
|
std::string status;
|
||||||
|
bool status_error{false};
|
||||||
|
|
||||||
|
private:
|
||||||
|
// methods
|
||||||
|
void render_list();
|
||||||
|
void render_popups();
|
||||||
|
void report(std::string const &Status, bool const Error = false);
|
||||||
|
// members
|
||||||
|
scene::layer_handle m_selected{null_handle}; // layer the buttons act on
|
||||||
|
char m_newname[128]{}; // file name of the layer being created
|
||||||
|
scene::layer_handle m_newparent{null_handle}; // layer which is going to include it
|
||||||
|
scene::layer_handle m_mergetarget{null_handle};
|
||||||
|
std::string m_popuperror;
|
||||||
|
};
|
||||||
|
|
||||||
|
// editor of the descriptions of *.inc scenery templates, see editorIncludeInfo.hpp
|
||||||
|
class includes_panel : public ui_panel
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
includes_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
|
||||||
|
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// methods
|
||||||
|
void open(std::string const &File);
|
||||||
|
// members
|
||||||
|
std::string m_file; // template being edited, relative to the scenery directory
|
||||||
|
char m_filename[256]{}; // name of a template typed in by hand
|
||||||
|
include_info m_info;
|
||||||
|
bool m_loaded{false};
|
||||||
|
std::string m_status;
|
||||||
|
bool m_statuserror{false};
|
||||||
|
};
|
||||||
|
|
||||||
class functions_panel : public ui_panel
|
class functions_panel : public ui_panel
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
509
editor/editorIncludeInfo.cpp
Normal file
509
editor/editorIncludeInfo.cpp
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
/*
|
||||||
|
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 "editor/editorIncludeInfo.hpp"
|
||||||
|
|
||||||
|
#include "utilities/Globals.h"
|
||||||
|
#include "utilities/utilities.h"
|
||||||
|
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::string const linemark{"//$e"};
|
||||||
|
|
||||||
|
// template file split into the description and the rest
|
||||||
|
struct template_text
|
||||||
|
{
|
||||||
|
std::string content; // whole file
|
||||||
|
std::string yaml; // description, with the line marks taken off; utf-8
|
||||||
|
std::vector<std::pair<std::size_t, std::size_t>> lines; // ranges of the content taken by the description lines, line ends included
|
||||||
|
std::string eol{"\r\n"};
|
||||||
|
std::size_t start{0}; // location of the first character past the byte order mark
|
||||||
|
bool utf8{false}; // the file is utf-8 encoded, rather than windows-1250 like most of the scenery files
|
||||||
|
};
|
||||||
|
|
||||||
|
// true if specified text is valid utf-8. HasExtended: set if the text has any characters outside of ascii
|
||||||
|
bool is_utf8(std::string const &Text, bool &HasExtended)
|
||||||
|
{
|
||||||
|
HasExtended = false;
|
||||||
|
for (std::size_t idx = 0; idx < Text.size();)
|
||||||
|
{
|
||||||
|
auto const lead{static_cast<unsigned char>(Text[idx])};
|
||||||
|
if (lead < 0x80)
|
||||||
|
{
|
||||||
|
++idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HasExtended = true;
|
||||||
|
auto const length{(lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 0};
|
||||||
|
if (length == 0 || idx + length > Text.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (auto offset = 1; offset < length; ++offset)
|
||||||
|
{
|
||||||
|
if ((static_cast<unsigned char>(Text[idx + offset]) & 0xC0) != 0x80)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx += length;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// counterpart of win1250_to_utf8(). characters outside of the supported set are replaced with question marks
|
||||||
|
std::string utf8_to_win1250(std::string const &Text)
|
||||||
|
{
|
||||||
|
// polish letters, as utf-8 sequences; the same set win1250_to_utf8() handles
|
||||||
|
static std::unordered_map<std::string, char> const charmap{{"\xC4\x84", '\xA5'}, {"\xC4\x86", '\xC6'}, {"\xC4\x98", '\xCA'}, {"\xC5\x81", '\xA3'}, {"\xC5\x83", '\xD1'}, {"\xC3\x93", '\xD3'},
|
||||||
|
{"\xC5\x9A", '\x8C'}, {"\xC5\xB9", '\x8F'}, {"\xC5\xBB", '\xAF'}, {"\xC4\x85", '\xB9'}, {"\xC4\x87", '\xE6'}, {"\xC4\x99", '\xEA'},
|
||||||
|
{"\xC5\x82", '\xB3'}, {"\xC5\x84", '\xF1'}, {"\xC3\xB3", '\xF3'}, {"\xC5\x9B", '\x9C'}, {"\xC5\xBA", '\x9F'}, {"\xC5\xBC", '\xBF'}};
|
||||||
|
std::string output;
|
||||||
|
for (std::size_t idx = 0; idx < Text.size();)
|
||||||
|
{
|
||||||
|
auto const lead{static_cast<unsigned char>(Text[idx])};
|
||||||
|
if (lead < 0x80)
|
||||||
|
{
|
||||||
|
output += Text[idx++];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const length{std::min<std::size_t>((lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 1, Text.size() - idx)};
|
||||||
|
auto const lookup{charmap.find(Text.substr(idx, length))};
|
||||||
|
output += (lookup != charmap.end() ? lookup->second : '?');
|
||||||
|
idx += length;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_template(std::string const &File, template_text &Text, std::string &Error)
|
||||||
|
{
|
||||||
|
std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary};
|
||||||
|
if (false == input.is_open())
|
||||||
|
{
|
||||||
|
Error = "can't open file \"" + File + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &content{Text.content};
|
||||||
|
content.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
|
||||||
|
|
||||||
|
auto const bom{content.compare(0, 3, "\xEF\xBB\xBF") == 0};
|
||||||
|
Text.start = bom ? 3 : 0;
|
||||||
|
bool extended;
|
||||||
|
Text.utf8 = bom || (is_utf8(content, extended) && extended);
|
||||||
|
if (auto const linebreak{content.find('\n')}; linebreak != std::string::npos)
|
||||||
|
{
|
||||||
|
Text.eol = (linebreak > 0 && content[linebreak - 1] == '\r') ? "\r\n" : "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto linebegin{Text.start}; linebegin < content.size();)
|
||||||
|
{
|
||||||
|
auto const linebreak{content.find('\n', linebegin)};
|
||||||
|
auto const next{linebreak == std::string::npos ? content.size() : linebreak + 1};
|
||||||
|
if (content.compare(linebegin, linemark.size(), linemark) == 0)
|
||||||
|
{
|
||||||
|
auto line{content.substr(linebegin + linemark.size(), next - linebegin - linemark.size())};
|
||||||
|
line.erase(line.find_last_not_of("\r\n") + 1);
|
||||||
|
// a single space separates the mark from the document; the rest is indentation, which carries meaning
|
||||||
|
if (false == line.empty() && line.front() == ' ')
|
||||||
|
{
|
||||||
|
line.erase(0, 1);
|
||||||
|
}
|
||||||
|
Text.yaml += line + '\n';
|
||||||
|
Text.lines.emplace_back(linebegin, next);
|
||||||
|
}
|
||||||
|
linebegin = next;
|
||||||
|
}
|
||||||
|
if (false == is_utf8(Text.yaml, extended))
|
||||||
|
{
|
||||||
|
Text.yaml = win1250_to_utf8(Text.yaml);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of the parameter if specified text holds (pN) placeholder at specified location, 0 otherwise. Length: size of the placeholder
|
||||||
|
int placeholder(std::string const &Text, std::size_t const Location, std::size_t &Length)
|
||||||
|
{
|
||||||
|
if (Text.compare(Location, 2, "(p") != 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto idx{Location + 2};
|
||||||
|
auto id{0};
|
||||||
|
while (idx < Text.size() && std::isdigit(static_cast<unsigned char>(Text[idx])) && id < 1000)
|
||||||
|
{
|
||||||
|
id = id * 10 + (Text[idx] - '0');
|
||||||
|
++idx;
|
||||||
|
}
|
||||||
|
if (idx == Location + 2 || idx >= Text.size() || Text[idx] != ')')
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Length = idx + 1 - Location;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of the parameter if the token is nothing but its placeholder, 0 otherwise
|
||||||
|
int exact_placeholder(std::string const &Token)
|
||||||
|
{
|
||||||
|
std::size_t length{0};
|
||||||
|
auto const id{placeholder(Token, 0, length)};
|
||||||
|
return length == Token.size() ? id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of the first parameter whose placeholder is a part of the token, 0 if there's none
|
||||||
|
int first_placeholder(std::string const &Token)
|
||||||
|
{
|
||||||
|
std::size_t length{0};
|
||||||
|
for (auto location{Token.find("(p")}; location != std::string::npos; location = Token.find("(p", location + 1))
|
||||||
|
{
|
||||||
|
if (auto const id{placeholder(Token, location, length)}; id != 0)
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokens of scenery text, in lower case and with the comments left out
|
||||||
|
std::vector<std::string> tokenize(std::string const &Text)
|
||||||
|
{
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
std::string token;
|
||||||
|
auto const flush = [&]() {
|
||||||
|
if (false == token.empty())
|
||||||
|
{
|
||||||
|
tokens.emplace_back(ToLower(token));
|
||||||
|
token.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (std::size_t idx = 0; idx < Text.size(); ++idx)
|
||||||
|
{
|
||||||
|
if (Text.compare(idx, 2, "//") == 0)
|
||||||
|
{
|
||||||
|
flush();
|
||||||
|
idx = Text.find('\n', idx);
|
||||||
|
if (idx == std::string::npos)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Text.compare(idx, 2, "/*") == 0)
|
||||||
|
{
|
||||||
|
flush();
|
||||||
|
idx = Text.find("*/", idx + 2);
|
||||||
|
if (idx == std::string::npos)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++idx;
|
||||||
|
}
|
||||||
|
else if (Text[idx] == ' ' || Text[idx] == '\t' || Text[idx] == '\n' || Text[idx] == '\r' || Text[idx] == ';')
|
||||||
|
{
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token += Text[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
include_parameter &include_info::parameter(int const Id)
|
||||||
|
{
|
||||||
|
auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })};
|
||||||
|
if (lookup == std::end(parameters) || lookup->id != Id)
|
||||||
|
{
|
||||||
|
lookup = parameters.emplace(lookup);
|
||||||
|
lookup->id = Id;
|
||||||
|
}
|
||||||
|
return *lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace editor_includes
|
||||||
|
{
|
||||||
|
|
||||||
|
std::vector<std::string> const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"};
|
||||||
|
|
||||||
|
bool load(std::string const &File, include_info &Info, std::string &Error)
|
||||||
|
{
|
||||||
|
Info = include_info();
|
||||||
|
template_text text;
|
||||||
|
if (false == read_template(File, text, Error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto const document{YAML::Load(text.yaml)};
|
||||||
|
if (document.IsNull())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (false == document.IsMap())
|
||||||
|
{
|
||||||
|
Error = "the description isn't a list of named values";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Info.name = document["name"].as<std::string>("");
|
||||||
|
Info.category = document["category"].as<std::string>("");
|
||||||
|
Info.description = document["description"].as<std::string>("");
|
||||||
|
if (auto const parameters{document["params"]}; parameters.IsDefined() && parameters.IsSequence())
|
||||||
|
{
|
||||||
|
for (auto const &entry : parameters)
|
||||||
|
{
|
||||||
|
auto const id{entry.IsMap() ? entry["id"].as<int>(0) : 0};
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto ¶meter{Info.parameter(id)};
|
||||||
|
parameter.role = entry["role"].as<std::string>("free");
|
||||||
|
parameter.label = entry["label"].as<std::string>("");
|
||||||
|
parameter.value = entry["default"].as<std::string>("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (YAML::Exception const &exception)
|
||||||
|
{
|
||||||
|
Error = std::string{"the description is malformed: "} + exception.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save(std::string const &File, include_info const &Info, std::string &Error)
|
||||||
|
{
|
||||||
|
template_text text;
|
||||||
|
if (false == read_template(File, text, Error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string yaml;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// start with the existing description, to keep the entries we don't know
|
||||||
|
auto document{YAML::Load(text.yaml)};
|
||||||
|
if (false == document.IsNull() && false == document.IsMap())
|
||||||
|
{
|
||||||
|
Error = "the existing description isn't a list of named values";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (false == document["version"].IsDefined())
|
||||||
|
{
|
||||||
|
document["version"] = 1;
|
||||||
|
}
|
||||||
|
auto const set = [&](char const *Key, std::string const &Value) {
|
||||||
|
if (false == Value.empty())
|
||||||
|
{
|
||||||
|
document[Key] = Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
document.remove(Key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
set("name", Info.name);
|
||||||
|
set("category", Info.category);
|
||||||
|
set("description", Info.description);
|
||||||
|
|
||||||
|
YAML::Node parameters{YAML::NodeType::Sequence};
|
||||||
|
for (auto const ¶meter : Info.parameters)
|
||||||
|
{
|
||||||
|
YAML::Node entry{YAML::NodeType::Map};
|
||||||
|
if (auto const existing{document["params"]}; existing.IsDefined() && existing.IsSequence())
|
||||||
|
{
|
||||||
|
for (auto const &candidate : existing)
|
||||||
|
{
|
||||||
|
if (candidate.IsMap() && candidate["id"].as<int>(0) == parameter.id)
|
||||||
|
{
|
||||||
|
entry = YAML::Clone(candidate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry["id"] = parameter.id;
|
||||||
|
entry["role"] = parameter.role.empty() ? std::string{"free"} : parameter.role;
|
||||||
|
if (false == parameter.label.empty())
|
||||||
|
{
|
||||||
|
entry["label"] = parameter.label;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry.remove("label");
|
||||||
|
}
|
||||||
|
if (false == parameter.value.empty())
|
||||||
|
{
|
||||||
|
entry["default"] = parameter.value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry.remove("default");
|
||||||
|
}
|
||||||
|
entry.SetStyle(YAML::EmitterStyle::Flow);
|
||||||
|
parameters.push_back(entry);
|
||||||
|
}
|
||||||
|
if (parameters.size() > 0)
|
||||||
|
{
|
||||||
|
document["params"] = parameters;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
document.remove("params");
|
||||||
|
}
|
||||||
|
|
||||||
|
YAML::Emitter emitter;
|
||||||
|
emitter << document;
|
||||||
|
yaml = emitter.c_str();
|
||||||
|
}
|
||||||
|
catch (YAML::Exception const &exception)
|
||||||
|
{
|
||||||
|
Error = std::string{"the existing description is malformed, fix or remove it first: "} + exception.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (false == text.utf8)
|
||||||
|
{
|
||||||
|
yaml = utf8_to_win1250(yaml);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string block;
|
||||||
|
std::istringstream lines{yaml};
|
||||||
|
for (std::string line; std::getline(lines, line);)
|
||||||
|
{
|
||||||
|
block += linemark + (line.empty() ? "" : " " + line) + text.eol;
|
||||||
|
}
|
||||||
|
// the description replaces the old one, or goes to the top of the file if there was none
|
||||||
|
auto content{text.content};
|
||||||
|
auto location{text.lines.empty() ? text.start : text.lines.front().first};
|
||||||
|
for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line)
|
||||||
|
{
|
||||||
|
content.erase(line->first, line->second - line->first);
|
||||||
|
}
|
||||||
|
content.insert(location, block);
|
||||||
|
|
||||||
|
auto const filepath{Global.asCurrentSceneryPath + File};
|
||||||
|
std::ofstream output{filepath + ".tmp", std::ios_base::binary | std::ios_base::trunc};
|
||||||
|
output << content;
|
||||||
|
output.close();
|
||||||
|
std::error_code error;
|
||||||
|
if (output.fail())
|
||||||
|
{
|
||||||
|
std::filesystem::remove(filepath + ".tmp", error);
|
||||||
|
Error = "can't write file \"" + File + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// the first save of a file leaves its original content next to it
|
||||||
|
if (false == std::filesystem::exists(filepath + ".bak", error))
|
||||||
|
{
|
||||||
|
std::filesystem::copy_file(filepath, filepath + ".bak", error);
|
||||||
|
}
|
||||||
|
std::filesystem::rename(filepath + ".tmp", filepath, error);
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
Error = "can't replace file \"" + File + "\": " + error.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parameter_count(std::string const &File)
|
||||||
|
{
|
||||||
|
template_text text;
|
||||||
|
std::string error;
|
||||||
|
if (false == read_template(File, text, error))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// the description lines may mention parameters the template doesn't use
|
||||||
|
auto content{text.content};
|
||||||
|
for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line)
|
||||||
|
{
|
||||||
|
content.erase(line->first, line->second - line->first);
|
||||||
|
}
|
||||||
|
auto count{0};
|
||||||
|
std::size_t length{0};
|
||||||
|
for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1))
|
||||||
|
{
|
||||||
|
count = std::max(count, placeholder(content, location, length));
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void suggest(std::string const &File, include_info &Info)
|
||||||
|
{
|
||||||
|
template_text text;
|
||||||
|
std::string error;
|
||||||
|
if (false == read_template(File, text, error))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto const tokens{tokenize(text.content)};
|
||||||
|
auto const assign = [&](std::size_t const Index, char const *Role, bool const Exact = true) {
|
||||||
|
if (Index >= tokens.size())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto const id{Exact ? exact_placeholder(tokens[Index]) : first_placeholder(tokens[Index])};
|
||||||
|
if (id != 0 && Info.parameter(id).role == "free")
|
||||||
|
{
|
||||||
|
Info.parameter(id).role = Role;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (std::size_t idx = 0; idx < tokens.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const &token{tokens[idx]};
|
||||||
|
if (token == "origin")
|
||||||
|
{
|
||||||
|
assign(idx + 1, "pos.x");
|
||||||
|
assign(idx + 2, "pos.y");
|
||||||
|
assign(idx + 3, "pos.z");
|
||||||
|
}
|
||||||
|
else if (token == "rotate")
|
||||||
|
{
|
||||||
|
assign(idx + 1, "rot.x");
|
||||||
|
assign(idx + 2, "rot.y");
|
||||||
|
assign(idx + 3, "rot.z");
|
||||||
|
}
|
||||||
|
else if (token == "node" && idx + 4 < tokens.size())
|
||||||
|
{
|
||||||
|
// node <max> <min> <name> <type> <x> <y> <z> ...
|
||||||
|
assign(idx + 3, "name", false);
|
||||||
|
auto const &type{tokens[idx + 4]};
|
||||||
|
if (type == "model" || type == "memcell" || type == "eventlauncher" || type == "sound")
|
||||||
|
{
|
||||||
|
assign(idx + 5, "pos.x");
|
||||||
|
assign(idx + 6, "pos.y");
|
||||||
|
assign(idx + 7, "pos.z");
|
||||||
|
}
|
||||||
|
if (type == "model")
|
||||||
|
{
|
||||||
|
assign(idx + 8, "rot.y");
|
||||||
|
assign(idx + 9, "model");
|
||||||
|
assign(idx + 10, "texture");
|
||||||
|
}
|
||||||
|
if (type == "memcell")
|
||||||
|
{
|
||||||
|
assign(idx + 11, "track");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace editor_includes
|
||||||
59
editor/editorIncludeInfo.hpp
Normal file
59
editor/editorIncludeInfo.hpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// description of an *.inc scenery template: what the template is, and what its (pN) parameters mean.
|
||||||
|
// it's kept in the template file itself, as a YAML document made of the comment lines starting with "//$e", e.g.
|
||||||
|
// //$e version: 1
|
||||||
|
// //$e name: Semaphore, 5 lights
|
||||||
|
// //$e params:
|
||||||
|
// //$e - {id: 1, role: name, label: Name}
|
||||||
|
// being a comment it's invisible to the scenery parser and to the older tools
|
||||||
|
struct include_parameter
|
||||||
|
{
|
||||||
|
int id{0}; // number of the (pN) placeholder
|
||||||
|
std::string role{"free"}; // what the value is; one of editor_includes::roles
|
||||||
|
std::string label; // name to show to the user
|
||||||
|
std::string value; // default value
|
||||||
|
};
|
||||||
|
|
||||||
|
struct include_info
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::string category;
|
||||||
|
std::string description;
|
||||||
|
std::vector<include_parameter> parameters;
|
||||||
|
|
||||||
|
// description of specified parameter, created if there's none
|
||||||
|
include_parameter ¶meter(int Id);
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace editor_includes
|
||||||
|
{
|
||||||
|
|
||||||
|
// recognized parameter roles. "free" is a value typed in by the user, with no meaning to the editor;
|
||||||
|
// the placement roles (pos.*, rot.*) and "name" are the ones the editor can fill in on its own
|
||||||
|
extern std::vector<std::string> const roles;
|
||||||
|
|
||||||
|
// reads description of specified template, located in the scenery directory. a template without the description
|
||||||
|
// yields an empty one. returns: false if the file can't be read or its description isn't a valid YAML document
|
||||||
|
bool load(std::string const &File, include_info &Info, std::string &Error);
|
||||||
|
// stores provided description in specified template. entries of the existing description which aren't
|
||||||
|
// a part of include_info are preserved. returns: false if the file can't be updated
|
||||||
|
bool save(std::string const &File, include_info const &Info, std::string &Error);
|
||||||
|
// highest number of (pN) placeholder used by specified template, 0 if it has none
|
||||||
|
int parameter_count(std::string const &File);
|
||||||
|
// sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone
|
||||||
|
void suggest(std::string const &File, include_info &Info);
|
||||||
|
|
||||||
|
} // namespace editor_includes
|
||||||
@@ -14,22 +14,44 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include "model/AnimModel.h"
|
#include "model/AnimModel.h"
|
||||||
#include "world/Track.h"
|
#include "world/Track.h"
|
||||||
#include "world/Traction.h"
|
#include "world/Traction.h"
|
||||||
|
#include "utilities/Globals.h"
|
||||||
|
#include "utilities/utilities.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace scene
|
namespace scene
|
||||||
{
|
{
|
||||||
|
|
||||||
node_layers Layers;
|
node_layers Layers;
|
||||||
|
|
||||||
|
// converts world space location to the one which produces it when loaded with this context in effect
|
||||||
|
glm::dvec3 layer_context::to_local(glm::dvec3 Location) const
|
||||||
|
{
|
||||||
|
// reverse of the transformation done by the scenery loader: rotation around the vertical axis, then scale, then offset
|
||||||
|
Location -= offset;
|
||||||
|
Location /= glm::dvec3{scale};
|
||||||
|
if (rotation != glm::vec3{0.f})
|
||||||
|
{
|
||||||
|
Location = glm::rotateY<double>(Location, -glm::radians(static_cast<double>(rotation.y)));
|
||||||
|
}
|
||||||
|
return Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool layer_context::matches(layer_context const &Other) const
|
||||||
|
{
|
||||||
|
return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f));
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t basic_layer::item_count() const
|
std::size_t basic_layer::item_count() const
|
||||||
{
|
{
|
||||||
return std::accumulate(std::begin(items), std::end(items), std::size_t{0});
|
return std::accumulate(std::begin(items), std::end(items), std::size_t{0});
|
||||||
}
|
}
|
||||||
|
|
||||||
// indicates start of content of specified scenery file. returns: handle to the layer of the file
|
// indicates start of content of the scenario file. returns: handle to its layer
|
||||||
layer_handle node_layers::open(std::string const &Name)
|
layer_handle node_layers::open(std::string const &Name)
|
||||||
{
|
{
|
||||||
// the same file can be included more than once, all its content goes to a single layer then
|
// the same file can be included more than once, all its content goes to a single layer then
|
||||||
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name; })};
|
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
|
||||||
auto layerhandle{static_cast<layer_handle>(std::distance(std::begin(m_layers), lookup) + 1)};
|
auto layerhandle{static_cast<layer_handle>(std::distance(std::begin(m_layers), lookup) + 1)};
|
||||||
if (lookup == std::end(m_layers))
|
if (lookup == std::end(m_layers))
|
||||||
{
|
{
|
||||||
@@ -38,6 +60,7 @@ layer_handle node_layers::open(std::string const &Name)
|
|||||||
m_layers.emplace_back();
|
m_layers.emplace_back();
|
||||||
m_layers.back().name = Name;
|
m_layers.back().name = Name;
|
||||||
m_layers.back().parent = handle();
|
m_layers.back().parent = handle();
|
||||||
|
stat(layerhandle);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -45,18 +68,39 @@ layer_handle node_layers::open(std::string const &Name)
|
|||||||
layerhandle = handle();
|
layerhandle = handle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (valid(layerhandle) && layerhandle != handle())
|
||||||
|
{
|
||||||
|
layer(layerhandle).context_begin = m_context;
|
||||||
|
}
|
||||||
// NOTE: the stack receives an entry even if the layer wasn't created, to keep it in sync with close() calls
|
// NOTE: the stack receives an entry even if the layer wasn't created, to keep it in sync with close() calls
|
||||||
m_stack.emplace_back(layerhandle);
|
m_stack.emplace_back(layerhandle);
|
||||||
|
|
||||||
return layerhandle;
|
return layerhandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
|
||||||
|
layer_handle node_layers::open(std::string const &Name, include_site Site)
|
||||||
|
{
|
||||||
|
Site.parent = handle();
|
||||||
|
auto const layerhandle{open(Name)};
|
||||||
|
if (valid(layerhandle) && layerhandle != Site.parent)
|
||||||
|
{
|
||||||
|
layer(layerhandle).sites.emplace_back(Site);
|
||||||
|
}
|
||||||
|
return layerhandle;
|
||||||
|
}
|
||||||
|
|
||||||
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
|
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
|
||||||
layer_handle node_layers::close()
|
layer_handle node_layers::close()
|
||||||
{
|
{
|
||||||
if (false == m_stack.empty())
|
if (false == m_stack.empty())
|
||||||
{
|
{
|
||||||
|
auto const closing{m_stack.back()};
|
||||||
m_stack.pop_back();
|
m_stack.pop_back();
|
||||||
|
if (valid(closing) && closing != handle())
|
||||||
|
{
|
||||||
|
layer(closing).context_end = m_context;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
@@ -66,22 +110,59 @@ void node_layers::clear()
|
|||||||
m_layers.clear();
|
m_layers.clear();
|
||||||
m_stack.clear();
|
m_stack.clear();
|
||||||
m_active = null_handle;
|
m_active = null_handle;
|
||||||
|
m_context = layer_context();
|
||||||
|
m_sources.clear();
|
||||||
|
m_erased.clear();
|
||||||
|
m_templates.clear();
|
||||||
|
m_terraindirective = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
|
||||||
|
layer_handle node_layers::resolve(layer_handle Layer) const
|
||||||
|
{
|
||||||
|
// NOTE: merge() rules out cycles, the counter is only a safeguard
|
||||||
|
for (auto hops = 0; hops < 64 && valid(Layer) && layer(Layer).merged != null_handle; ++hops)
|
||||||
|
{
|
||||||
|
Layer = layer(Layer).merged;
|
||||||
|
}
|
||||||
|
return Layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
int node_layers::depth(layer_handle Layer) const
|
int node_layers::depth(layer_handle Layer) const
|
||||||
{
|
{
|
||||||
auto depth{0};
|
auto depth{0};
|
||||||
// NOTE: parent is always created before its children, which rules out cycles
|
Layer = resolve(Layer);
|
||||||
while (valid(Layer) && layer(Layer).parent != null_handle && layer(Layer).parent < Layer)
|
while (valid(Layer) && layer(Layer).parent != null_handle && depth < 64)
|
||||||
{
|
{
|
||||||
Layer = layer(Layer).parent;
|
Layer = resolve(layer(Layer).parent);
|
||||||
++depth;
|
++depth;
|
||||||
}
|
}
|
||||||
return depth;
|
return depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
void node_layers::count(layer_handle const Layer, layer_item const Item, int const Change)
|
std::vector<layer_handle> node_layers::tree() const
|
||||||
{
|
{
|
||||||
|
std::vector<layer_handle> order;
|
||||||
|
order.reserve(m_layers.size());
|
||||||
|
// NOTE: the layers are few, plain scans do
|
||||||
|
std::function<void(layer_handle)> const add = [&](layer_handle const Parent) {
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
if (listed(candidate) && resolve(layer(candidate).parent) == Parent && order.size() < m_layers.size())
|
||||||
|
{
|
||||||
|
order.emplace_back(candidate);
|
||||||
|
add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
add(null_handle);
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
void node_layers::count(layer_handle Layer, layer_item const Item, int const Change)
|
||||||
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
if (false == valid(Layer))
|
if (false == valid(Layer))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -97,9 +178,10 @@ void node_layers::count(layer_handle const Layer, layer_item const Item, int con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void node_layers::active(layer_handle const Layer)
|
void node_layers::active(layer_handle Layer)
|
||||||
{
|
{
|
||||||
if (false == valid(Layer))
|
Layer = resolve(Layer);
|
||||||
|
if (false == valid(Layer) || layer(Layer).removed || false == writable(Layer))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,16 +192,17 @@ void node_layers::active(layer_handle const Layer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
|
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
|
||||||
void node_layers::visible(layer_handle const Layer, bool const Visible)
|
void node_layers::visible(layer_handle Layer, bool const Visible)
|
||||||
{
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
if (false == valid(Layer) || layer(Layer).visible == Visible)
|
if (false == valid(Layer) || layer(Layer).visible == Visible)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
layer(Layer).visible = Visible;
|
layer(Layer).visible = Visible;
|
||||||
|
|
||||||
auto const update_node = [Layer, Visible](basic_node *Node) {
|
auto const update_node = [this, Layer, Visible](basic_node *Node) {
|
||||||
if (Node == nullptr || Node->layer() != Layer)
|
if (Node == nullptr || resolve(Node->layer()) != Layer)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -153,19 +236,101 @@ void node_layers::visible(layer_handle const Layer, bool const Visible)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void node_layers::locked(layer_handle const Layer, bool const Locked)
|
void node_layers::locked(layer_handle Layer, bool const Locked)
|
||||||
{
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
if (valid(Layer))
|
if (valid(Layer))
|
||||||
{
|
{
|
||||||
layer(Layer).locked = Locked;
|
layer(Layer).locked = Locked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// true if nodes of specified layer can be selected and modified in the editor
|
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
|
||||||
bool node_layers::editable(layer_handle const Layer) const
|
bool node_layers::writable(layer_handle Layer, std::string *Reason) const
|
||||||
{
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
|
if (false == valid(Layer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const &target{layer(Layer)};
|
||||||
|
char const *reason{nullptr};
|
||||||
|
if (target.binary)
|
||||||
|
{
|
||||||
|
reason = "the file is loaded from binary terrain cache";
|
||||||
|
}
|
||||||
|
else if (target.sites.size() > 1)
|
||||||
|
{
|
||||||
|
// one definition yields several nodes, there's no telling which of them should be written back
|
||||||
|
reason = "the file is included more than once";
|
||||||
|
}
|
||||||
|
else if (false == target.sites.empty() && target.sites.front().parameters)
|
||||||
|
{
|
||||||
|
reason = "the file is included with parameters";
|
||||||
|
}
|
||||||
|
if (reason != nullptr && Reason != nullptr)
|
||||||
|
{
|
||||||
|
*Reason = reason;
|
||||||
|
}
|
||||||
|
return reason == nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// true if nodes of specified layer can be selected and modified in the editor
|
||||||
|
bool node_layers::editable(layer_handle Layer) const
|
||||||
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
// nodes without a layer (regular load, or created by the simulation) aren't restricted
|
// nodes without a layer (regular load, or created by the simulation) aren't restricted
|
||||||
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked);
|
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked && false == layer(Layer).removed && writable(Layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// true if specified node can be selected and modified in the editor. optionally explains why it can't
|
||||||
|
bool node_layers::editable(basic_node const *Node, std::string *Reason) const
|
||||||
|
{
|
||||||
|
if (Node == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const nodelayer{resolve(Node->layer())};
|
||||||
|
if (false == valid(nodelayer))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::string reason;
|
||||||
|
if (Node->m_template)
|
||||||
|
{
|
||||||
|
reason = "it's a part of an include template (.inc)";
|
||||||
|
}
|
||||||
|
else if (false == writable(nodelayer, &reason))
|
||||||
|
{
|
||||||
|
// reason was filled by the call
|
||||||
|
}
|
||||||
|
else if (layer(nodelayer).locked)
|
||||||
|
{
|
||||||
|
reason = "its layer is locked";
|
||||||
|
}
|
||||||
|
else if (false == layer(nodelayer).visible || layer(nodelayer).removed)
|
||||||
|
{
|
||||||
|
reason = "its layer is hidden";
|
||||||
|
}
|
||||||
|
if (false == reason.empty() && Reason != nullptr)
|
||||||
|
{
|
||||||
|
*Reason = reason;
|
||||||
|
}
|
||||||
|
return reason.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves specified node to specified layer
|
||||||
|
void node_layers::move(basic_node *Node, layer_handle Layer)
|
||||||
|
{
|
||||||
|
Layer = resolve(Layer);
|
||||||
|
if (Node == nullptr || false == valid(Layer) || resolve(Node->layer()) == Layer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// NOTE: the only nodes the editor creates and moves between layers are model instances
|
||||||
|
count(Node->layer(), layer_item::model, -1);
|
||||||
|
Node->layer(Layer);
|
||||||
|
count(Layer, layer_item::model, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<layer_handle> node_layers::hidden() const
|
std::vector<layer_handle> node_layers::hidden() const
|
||||||
@@ -185,10 +350,323 @@ void node_layers::show_all()
|
|||||||
{
|
{
|
||||||
for (auto const hiddenlayer : hidden())
|
for (auto const hiddenlayer : hidden())
|
||||||
{
|
{
|
||||||
visible(hiddenlayer, true);
|
// layers dropped from the scenery stay out of sight
|
||||||
|
if (false == layer(hiddenlayer).removed)
|
||||||
|
{
|
||||||
|
visible(hiddenlayer, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
|
||||||
|
void node_layers::track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles, glm::vec3 const &Scale)
|
||||||
|
{
|
||||||
|
if (Node == nullptr || Node->m_template || false == valid(Node->layer()) || false == Span.valid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_sources[Node] = {Node->layer(), Span, m_context, Node->location(), Angles, Scale};
|
||||||
|
}
|
||||||
|
|
||||||
|
// indicates specified node was removed from the scene
|
||||||
|
void node_layers::forget(basic_node const *Node)
|
||||||
|
{
|
||||||
|
auto const lookup{m_sources.find(Node)};
|
||||||
|
if (lookup == m_sources.end())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_erased.emplace_back(lookup->second.layer, lookup->second.span);
|
||||||
|
m_sources.erase(lookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) const
|
||||||
|
{
|
||||||
|
// NOTE: content of a merged layer, includes of other files among it, belongs to the layer it was merged into
|
||||||
|
for (auto hops = 0; hops < 64 && valid(Layer); ++hops)
|
||||||
|
{
|
||||||
|
Layer = resolve(layer(Layer).parent);
|
||||||
|
if (Layer == Ancestor)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string node_layers::path(layer_handle const Layer) const
|
||||||
|
{
|
||||||
|
return Global.asCurrentSceneryPath + layer(Layer).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stores current state of the layer file, to detect later whether it was changed behind our back
|
||||||
|
void node_layers::stat(layer_handle const Layer)
|
||||||
|
{
|
||||||
|
std::error_code error;
|
||||||
|
auto const filepath{std::filesystem::path(path(Layer))};
|
||||||
|
auto const filesize{std::filesystem::file_size(filepath, error)};
|
||||||
|
layer(Layer).filesize = error ? 0 : filesize;
|
||||||
|
auto const filetime{std::filesystem::last_write_time(filepath, error)};
|
||||||
|
layer(Layer).filetime = error ? std::filesystem::file_time_type{} : filetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
|
||||||
|
layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason)
|
||||||
|
{
|
||||||
|
Parent = resolve(Parent);
|
||||||
|
if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary)
|
||||||
|
{
|
||||||
|
Reason = "the parent layer can't include other files";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
// file names in the scenery are processed in lower case, with forward slashes
|
||||||
|
Name = ToLower(Name);
|
||||||
|
replace_slashes(Name);
|
||||||
|
Name.erase(0, Name.find_first_not_of(" \t"));
|
||||||
|
Name.erase(Name.find_last_not_of(" \t") + 1);
|
||||||
|
if (Name.empty())
|
||||||
|
{
|
||||||
|
Reason = "the layer needs a file name";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
if (Name.find_first_of(" \t\";:*?<>|") != std::string::npos || Name.front() == '/' || Name.back() == '/' || contains(Name, ".."))
|
||||||
|
{
|
||||||
|
Reason = "the file name has to be a plain path inside the scenery directory, without spaces";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
if (std::filesystem::path(Name).extension().empty())
|
||||||
|
{
|
||||||
|
Name += ".scm";
|
||||||
|
}
|
||||||
|
if (Name.ends_with(".inc"))
|
||||||
|
{
|
||||||
|
Reason = ".inc files are include templates, not layers";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
if (m_layers.size() >= static_cast<std::size_t>(std::numeric_limits<layer_handle>::max()))
|
||||||
|
{
|
||||||
|
Reason = "too many layers";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
auto const taken{std::any_of(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
|
||||||
|
if (taken || FileExists(Global.asCurrentSceneryPath + Name))
|
||||||
|
{
|
||||||
|
Reason = "the file \"" + Name + "\" already exists";
|
||||||
|
return null_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the include directive goes after the last file included by the parent, so the scenery files stay ahead of
|
||||||
|
// what may follow them in a scenario, like the vehicles
|
||||||
|
auto anchor{null_handle};
|
||||||
|
std::streamoff anchorend{-1};
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const &sibling{m_layers[idx]};
|
||||||
|
if (sibling.dead || sibling.created || sibling.sites.size() != 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const &site{sibling.sites.front()};
|
||||||
|
if (site.parent == Parent && false == site.fixed && site.span.end > anchorend)
|
||||||
|
{
|
||||||
|
anchor = static_cast<layer_handle>(idx + 1);
|
||||||
|
anchorend = site.span.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_layers.emplace_back();
|
||||||
|
auto const layerhandle{static_cast<layer_handle>(m_layers.size())};
|
||||||
|
auto &created{m_layers.back()};
|
||||||
|
created.name = Name;
|
||||||
|
created.parent = Parent;
|
||||||
|
created.created = true;
|
||||||
|
created.editormade = true;
|
||||||
|
created.anchor = anchor;
|
||||||
|
// placement the new file is going to be loaded with
|
||||||
|
created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_end;
|
||||||
|
created.context_end = created.context_begin;
|
||||||
|
|
||||||
|
active(layerhandle);
|
||||||
|
|
||||||
|
return layerhandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const
|
||||||
|
{
|
||||||
|
if (false == listed(Layer) || layer(Layer).removed)
|
||||||
|
{
|
||||||
|
Reason = "no such layer";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const &target{layer(Layer)};
|
||||||
|
if (target.created)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (target.sites.empty())
|
||||||
|
{
|
||||||
|
Reason = "the scenario file can't be removed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target.binary)
|
||||||
|
{
|
||||||
|
Reason = "the file is loaded from binary terrain cache";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target.sites.size() > 1)
|
||||||
|
{
|
||||||
|
Reason = "the file is included more than once";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target.sites.front().fixed)
|
||||||
|
{
|
||||||
|
Reason = "the file is included by an include template (.inc)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
|
||||||
|
bool node_layers::remove(layer_handle Layer)
|
||||||
|
{
|
||||||
|
std::string reason;
|
||||||
|
if (false == can_remove(Layer, reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
// layers merged elsewhere keep going, their content is no longer a part of the removed file
|
||||||
|
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visible(candidate, false);
|
||||||
|
layer(candidate).removed = true;
|
||||||
|
if (m_active == candidate)
|
||||||
|
{
|
||||||
|
m_active = null_handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_active == null_handle)
|
||||||
|
{
|
||||||
|
// fall back on the scenario file, which is always there
|
||||||
|
active(1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes back remove() which wasn't saved yet
|
||||||
|
void node_layers::restore(layer_handle Layer)
|
||||||
|
{
|
||||||
|
if (false == valid(Layer) || false == layer(Layer).removed || layer(Layer).dead)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// a layer can't be brought back without the file which includes it
|
||||||
|
auto const parent{resolve(layer(Layer).parent)};
|
||||||
|
if (valid(parent) && layer(parent).removed)
|
||||||
|
{
|
||||||
|
restore(parent);
|
||||||
|
}
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
layer(candidate).removed = false;
|
||||||
|
visible(candidate, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const
|
||||||
|
{
|
||||||
|
if (false == listed(Source) || false == listed(Target) || layer(Source).removed || layer(Target).removed)
|
||||||
|
{
|
||||||
|
Reason = "no such layer";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Source == Target)
|
||||||
|
{
|
||||||
|
Reason = "pick another layer to merge into";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const &source{layer(Source)};
|
||||||
|
auto const &target{layer(Target)};
|
||||||
|
if (false == writable(Target, &Reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (is_descendant(Target, Source))
|
||||||
|
{
|
||||||
|
// the merged text would include the file it was put in
|
||||||
|
Reason = "the target layer is included by the merged one";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (source.created)
|
||||||
|
{
|
||||||
|
// nothing but the nodes made in the editor to move
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (source.sites.empty())
|
||||||
|
{
|
||||||
|
Reason = "the scenario file can't be merged into another layer";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (false == writable(Source, &Reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (source.sites.front().fixed)
|
||||||
|
{
|
||||||
|
Reason = "the file is included by an include template (.inc)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resolve(source.sites.front().parent) != Target)
|
||||||
|
{
|
||||||
|
// the text is moved to the end of another file. its statements have to keep their meaning there,
|
||||||
|
// and can't leave anything behind for what used to follow it
|
||||||
|
if (false == source.context_begin.matches(target.context_end) || false == source.context_end.matches(source.context_begin))
|
||||||
|
{
|
||||||
|
Reason = "the layers are loaded with different origin, rotation or scale";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves content of the source layer to the target layer
|
||||||
|
bool node_layers::merge(layer_handle Source, layer_handle Target)
|
||||||
|
{
|
||||||
|
std::string reason;
|
||||||
|
if (false == can_merge(Source, Target, reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// start with the same visibility on both sides, as it's kept per node
|
||||||
|
visible(Source, true);
|
||||||
|
visible(Target, true);
|
||||||
|
for (std::size_t idx = 0; idx < layer(Source).items.size(); ++idx)
|
||||||
|
{
|
||||||
|
layer(Target).items[idx] += layer(Source).items[idx];
|
||||||
|
}
|
||||||
|
layer(Source).merged = Target;
|
||||||
|
if (m_active == Source)
|
||||||
|
{
|
||||||
|
active(Target);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
|
||||||
|
bool node_layers::pending() const
|
||||||
|
{
|
||||||
|
return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); });
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace scene
|
} // namespace scene
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
|
|
||||||
#include "scene/scenenode.h"
|
#include "scene/scenenode.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
namespace scene
|
namespace scene
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -31,6 +34,39 @@ enum class layer_item
|
|||||||
count_
|
count_
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// placement applied to scenery statements by the origin, rotate and scale directives in effect
|
||||||
|
struct layer_context
|
||||||
|
{
|
||||||
|
glm::dvec3 offset{0.0};
|
||||||
|
glm::vec3 rotation{0.f};
|
||||||
|
glm::vec3 scale{1.f};
|
||||||
|
|
||||||
|
// converts world space location to the one which produces it when loaded with this context in effect
|
||||||
|
glm::dvec3 to_local(glm::dvec3 Location) const;
|
||||||
|
bool matches(layer_context const &Other) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// location of a piece of text in a scenery file, as byte offsets
|
||||||
|
struct source_span
|
||||||
|
{
|
||||||
|
std::streamoff begin{-1};
|
||||||
|
std::streamoff end{-1};
|
||||||
|
|
||||||
|
bool valid() const
|
||||||
|
{
|
||||||
|
return begin >= 0 && end > begin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// include directive bringing a scenery file into another one
|
||||||
|
struct include_site
|
||||||
|
{
|
||||||
|
layer_handle parent{null_handle}; // layer of the file holding the directive
|
||||||
|
source_span span; // location of the whole directive in that file
|
||||||
|
bool parameters{false}; // the file is included with parameters
|
||||||
|
bool fixed{false}; // the directive can't be rewritten: it's a part of an *.inc template, or its location is unknown
|
||||||
|
};
|
||||||
|
|
||||||
// scenery layer: content of a single scenery file (.scn, .scm, .ctr...) other than an .inc node template
|
// scenery layer: content of a single scenery file (.scn, .scm, .ctr...) other than an .inc node template
|
||||||
struct basic_layer
|
struct basic_layer
|
||||||
{
|
{
|
||||||
@@ -40,18 +76,51 @@ struct basic_layer
|
|||||||
bool locked{false}; // items of locked layer can't be selected in the editor
|
bool locked{false}; // items of locked layer can't be selected in the editor
|
||||||
bool binary{false}; // the file wasn't parsed, its content came from binary terrain file
|
bool binary{false}; // the file wasn't parsed, its content came from binary terrain file
|
||||||
std::array<std::size_t, static_cast<std::size_t>(layer_item::count_)> items{}; // amount of items of each kind
|
std::array<std::size_t, static_cast<std::size_t>(layer_item::count_)> items{}; // amount of items of each kind
|
||||||
|
// source data, used by the scenery save
|
||||||
|
std::vector<include_site> sites; // include directives bringing the file in; empty for the scenario file
|
||||||
|
layer_context context_begin; // placement in effect at the start of the file...
|
||||||
|
layer_context context_end; // ...and right after its end
|
||||||
|
std::uintmax_t filesize{0}; // state of the file when it was loaded or saved the last time
|
||||||
|
std::filesystem::file_time_type filetime{};
|
||||||
|
// pending changes, applied by the scenery save
|
||||||
|
bool created{false}; // layer was created in the editor, its file doesn't exist yet
|
||||||
|
bool editormade{false}; // the file was made by the editor during this session, there's no original content to keep a backup of
|
||||||
|
layer_handle anchor{null_handle}; // for created layer: sibling its include directive goes after
|
||||||
|
bool removed{false}; // layer is to be dropped from the scenery
|
||||||
|
layer_handle merged{null_handle}; // layer which took over the content of this one
|
||||||
|
bool dead{false}; // removal or merge of the layer was saved, it's no longer a part of the scenery
|
||||||
|
|
||||||
std::size_t item_count() const;
|
std::size_t item_count() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// origin of a node defined in a scenery layer file, with the placement it was loaded with
|
||||||
|
struct node_source
|
||||||
|
{
|
||||||
|
layer_handle layer{null_handle}; // layer of the file holding the node definition
|
||||||
|
source_span span; // location of the definition in that file
|
||||||
|
layer_context context; // placement in effect at the definition
|
||||||
|
glm::dvec3 location{0.0};
|
||||||
|
glm::vec3 angles{0.f};
|
||||||
|
glm::vec3 scale{1.f};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct save_result
|
||||||
|
{
|
||||||
|
bool success{false};
|
||||||
|
std::vector<std::string> files; // names of saved files
|
||||||
|
std::string message; // summary, or cause of the failure
|
||||||
|
};
|
||||||
|
|
||||||
// holds scenery layers, established during load of scenery opened for editing (-edit)
|
// holds scenery layers, established during load of scenery opened for editing (-edit)
|
||||||
// NOTE: regular load doesn't create any layers, all nodes are left with null_handle
|
// NOTE: regular load doesn't create any layers, all nodes are left with null_handle
|
||||||
class node_layers
|
class node_layers
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// methods
|
// methods
|
||||||
// indicates start of content of specified scenery file. returns: handle to the layer of the file
|
// indicates start of content of the scenario file. returns: handle to its layer
|
||||||
layer_handle open(std::string const &Name);
|
layer_handle open(std::string const &Name);
|
||||||
|
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
|
||||||
|
layer_handle open(std::string const &Name, include_site Site);
|
||||||
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
|
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
|
||||||
layer_handle close();
|
layer_handle close();
|
||||||
// returns layer of the scenery file being loaded, or null_handle if there's none
|
// returns layer of the scenery file being loaded, or null_handle if there's none
|
||||||
@@ -82,32 +151,110 @@ class node_layers
|
|||||||
{
|
{
|
||||||
return m_layers[Layer - 1];
|
return m_layers[Layer - 1];
|
||||||
}
|
}
|
||||||
|
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
|
||||||
|
layer_handle resolve(layer_handle Layer) const;
|
||||||
|
// true if the layer is a part of the scenery and should be presented to the user
|
||||||
|
bool listed(layer_handle const Layer) const
|
||||||
|
{
|
||||||
|
return valid(Layer) && false == layer(Layer).dead && layer(Layer).merged == null_handle;
|
||||||
|
}
|
||||||
// number of layers between specified layer and the scenario file
|
// number of layers between specified layer and the scenario file
|
||||||
int depth(layer_handle Layer) const;
|
int depth(layer_handle Layer) const;
|
||||||
|
// handles of the layers to present to the user, each followed by the layers it includes
|
||||||
|
std::vector<layer_handle> tree() const;
|
||||||
// adjusts item count of specified layer
|
// adjusts item count of specified layer
|
||||||
void count(layer_handle const Layer, layer_item const Item, int const Change = 1);
|
void count(layer_handle Layer, layer_item const Item, int const Change = 1);
|
||||||
// layer receiving the items created in the editor, or null_handle if there's none
|
// layer receiving the items created in the editor, or null_handle if there's none
|
||||||
layer_handle active() const
|
layer_handle active() const
|
||||||
{
|
{
|
||||||
return m_active;
|
return m_active;
|
||||||
}
|
}
|
||||||
// NOTE: active layer is made visible and unlocked
|
// NOTE: active layer is made visible and unlocked
|
||||||
void active(layer_handle const Layer);
|
void active(layer_handle Layer);
|
||||||
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
|
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
|
||||||
void visible(layer_handle const Layer, bool const Visible);
|
void visible(layer_handle Layer, bool const Visible);
|
||||||
void locked(layer_handle const Layer, bool const Locked);
|
void locked(layer_handle Layer, bool const Locked);
|
||||||
|
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
|
||||||
|
bool writable(layer_handle Layer, std::string *Reason = nullptr) const;
|
||||||
// true if nodes of specified layer can be selected and modified in the editor
|
// true if nodes of specified layer can be selected and modified in the editor
|
||||||
bool editable(layer_handle const Layer) const;
|
bool editable(layer_handle Layer) const;
|
||||||
|
// true if specified node can be selected and modified in the editor. optionally explains why it can't
|
||||||
|
bool editable(basic_node const *Node, std::string *Reason = nullptr) const;
|
||||||
|
// moves specified node to specified layer
|
||||||
|
void move(basic_node *Node, layer_handle Layer);
|
||||||
// handles of layers which are currently hidden
|
// handles of layers which are currently hidden
|
||||||
std::vector<layer_handle> hidden() const;
|
std::vector<layer_handle> hidden() const;
|
||||||
// restores visibility of all layers
|
// restores visibility of all layers
|
||||||
void show_all();
|
void show_all();
|
||||||
|
|
||||||
|
// scenery load bookkeeping
|
||||||
|
// registers *.inc template used by the scenery
|
||||||
|
void template_used(std::string const &Name)
|
||||||
|
{
|
||||||
|
m_templates.emplace(Name);
|
||||||
|
}
|
||||||
|
std::set<std::string> const &templates() const
|
||||||
|
{
|
||||||
|
return m_templates;
|
||||||
|
}
|
||||||
|
// updates placement in effect for the statements which follow
|
||||||
|
void context(layer_context const &Context)
|
||||||
|
{
|
||||||
|
m_context = Context;
|
||||||
|
}
|
||||||
|
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
|
||||||
|
void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f});
|
||||||
|
// indicates specified node was removed from the scene
|
||||||
|
void forget(basic_node const *Node);
|
||||||
|
// notes the scenery already holds the directive setting up terrain made in the editor
|
||||||
|
void terrain_directive(bool const Present)
|
||||||
|
{
|
||||||
|
m_terraindirective = Present;
|
||||||
|
}
|
||||||
|
bool terrain_directive() const
|
||||||
|
{
|
||||||
|
return m_terraindirective;
|
||||||
|
}
|
||||||
|
|
||||||
|
// layer management. changes are applied to the scenery files by save()
|
||||||
|
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
|
||||||
|
layer_handle create(std::string Name, layer_handle Parent, std::string &Reason);
|
||||||
|
bool can_remove(layer_handle Layer, std::string &Reason) const;
|
||||||
|
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
|
||||||
|
bool remove(layer_handle Layer);
|
||||||
|
// takes back remove() which wasn't saved yet
|
||||||
|
void restore(layer_handle Layer);
|
||||||
|
bool can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const;
|
||||||
|
// moves content of the source layer to the target layer
|
||||||
|
bool merge(layer_handle Source, layer_handle Target);
|
||||||
|
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
|
||||||
|
bool pending() const;
|
||||||
|
// true if specified layer is included, directly or not, by the other one
|
||||||
|
bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const;
|
||||||
|
|
||||||
|
// writes changes made in the editor to the scenery files: changed, created and deleted model instances and
|
||||||
|
// memory cells, and layer changes. the files are left intact unless the whole operation can be performed.
|
||||||
|
// Rootstatements: additional statements to place at the end of the scenario file
|
||||||
|
save_result save(std::vector<std::string> const &Rootstatements = {});
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// types
|
||||||
|
struct file_patch;
|
||||||
|
struct composition;
|
||||||
|
struct save_state;
|
||||||
|
// methods
|
||||||
|
std::string path(layer_handle const Layer) const;
|
||||||
|
void stat(layer_handle const Layer);
|
||||||
|
bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const;
|
||||||
// members
|
// members
|
||||||
std::vector<basic_layer> m_layers; // layer handle is index in the vector + 1
|
std::vector<basic_layer> m_layers; // layer handle is index in the vector + 1
|
||||||
std::vector<layer_handle> m_stack; // helper, layers of scenery files being loaded
|
std::vector<layer_handle> m_stack; // helper, layers of scenery files being loaded
|
||||||
layer_handle m_active{null_handle};
|
layer_handle m_active{null_handle};
|
||||||
|
layer_context m_context; // placement in effect at the current point of scenery load
|
||||||
|
std::unordered_map<basic_node const *, node_source> m_sources; // origins of nodes which can be rewritten on save
|
||||||
|
std::vector<std::pair<layer_handle, source_span>> m_erased; // definitions of nodes deleted since the load or the last save
|
||||||
|
std::set<std::string> m_templates; // *.inc templates used by the scenery
|
||||||
|
bool m_terraindirective{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
extern node_layers Layers;
|
extern node_layers Layers;
|
||||||
|
|||||||
972
scene/scenelayerssave.cpp
Normal file
972
scene/scenelayerssave.cpp
Normal file
@@ -0,0 +1,972 @@
|
|||||||
|
/*
|
||||||
|
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 "scene/scenelayers.h"
|
||||||
|
|
||||||
|
#include "simulation/simulation.h"
|
||||||
|
#include "model/AnimModel.h"
|
||||||
|
#include "world/MemCell.h"
|
||||||
|
#include "utilities/Globals.h"
|
||||||
|
#include "utilities/Logs.h"
|
||||||
|
#include "utilities/utilities.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
// scenery save for sceneries opened for editing.
|
||||||
|
// the scenery files are patched rather than generated: text of a file is carried over as it is, except for the
|
||||||
|
// definitions of nodes changed in the editor and the include directives affected by layer changes. this keeps
|
||||||
|
// everything the editor doesn't understand or didn't touch, comments and formatting included.
|
||||||
|
|
||||||
|
namespace scene
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct text_token
|
||||||
|
{
|
||||||
|
std::size_t begin;
|
||||||
|
std::size_t end;
|
||||||
|
std::string text; // lower case
|
||||||
|
};
|
||||||
|
|
||||||
|
struct text_change
|
||||||
|
{
|
||||||
|
std::size_t begin;
|
||||||
|
std::size_t end;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_separator(char const Char)
|
||||||
|
{
|
||||||
|
return Char == ' ' || Char == '\t' || Char == '\n' || Char == '\r' || Char == ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
// splits scenery text into tokens the same way the scenery parser does: comments are skipped, quoted text is kept together
|
||||||
|
std::vector<text_token> tokenize(std::string const &Text)
|
||||||
|
{
|
||||||
|
std::vector<text_token> tokens;
|
||||||
|
auto const size{Text.size()};
|
||||||
|
std::size_t idx{0};
|
||||||
|
auto const starts = [&](char const *Mark) { return Text.compare(idx, 2, Mark) == 0; };
|
||||||
|
while (idx < size)
|
||||||
|
{
|
||||||
|
if (is_separator(Text[idx]))
|
||||||
|
{
|
||||||
|
++idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (starts("//"))
|
||||||
|
{
|
||||||
|
idx = Text.find('\n', idx);
|
||||||
|
idx = (idx == std::string::npos ? size : idx + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (starts("/*"))
|
||||||
|
{
|
||||||
|
idx = Text.find("*/", idx + 2);
|
||||||
|
idx = (idx == std::string::npos ? size : idx + 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const begin{idx};
|
||||||
|
while (idx < size && false == is_separator(Text[idx]) && false == starts("//") && false == starts("/*"))
|
||||||
|
{
|
||||||
|
if (Text[idx] == '\"')
|
||||||
|
{
|
||||||
|
// quoted part runs up to the closing quote, whatever is inside
|
||||||
|
++idx;
|
||||||
|
while (idx < size && Text[idx] != '\"')
|
||||||
|
{
|
||||||
|
idx += (Text[idx] == '\\' ? 2 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++idx;
|
||||||
|
}
|
||||||
|
idx = std::min(idx, size);
|
||||||
|
tokens.push_back({begin, idx, ToLower(Text.substr(begin, idx - begin))});
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string number(double const Value)
|
||||||
|
{
|
||||||
|
std::ostringstream converter;
|
||||||
|
converter.imbue(std::locale::classic());
|
||||||
|
converter << std::fixed << std::setprecision(3) << Value;
|
||||||
|
auto text{converter.str()};
|
||||||
|
return text == "-0.000" ? "0.000" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string numbers(glm::dvec3 const &Values)
|
||||||
|
{
|
||||||
|
return number(Values.x) + ' ' + number(Values.y) + ' ' + number(Values.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply(std::string &Text, std::vector<text_change> Changes)
|
||||||
|
{
|
||||||
|
// from the end, so the locations of pending changes stay valid
|
||||||
|
std::sort(std::begin(Changes), std::end(Changes), [](text_change const &Left, text_change const &Right) { return Left.begin > Right.begin; });
|
||||||
|
for (auto const &change : Changes)
|
||||||
|
{
|
||||||
|
Text.replace(change.begin, change.end - change.begin, change.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// placement to write into definition of a model instance. empty parts are left as they are
|
||||||
|
struct model_placement
|
||||||
|
{
|
||||||
|
std::optional<glm::dvec3> location; // in the coordinates used by the definition
|
||||||
|
std::optional<glm::vec3> angles; // complete rotation of the instance
|
||||||
|
glm::vec3 rotation{0.f}; // rotation the loader adds to the angle specified in the definition
|
||||||
|
std::optional<glm::vec3> scale; // scale factor to put in the definition
|
||||||
|
};
|
||||||
|
|
||||||
|
bool about_equal(float const Left, float const Right)
|
||||||
|
{
|
||||||
|
return std::abs(Left - Right) < 1e-3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// definition layout: node <max> <min> <name> model <x> <y> <z> <angle> <model file> <texture> [options] endmodel
|
||||||
|
bool patch_model(std::string &Text, model_placement const &Placement, std::string &Error)
|
||||||
|
{
|
||||||
|
auto const tokens{tokenize(Text)};
|
||||||
|
if (tokens.size() < 12 || tokens[0].text != "node" || tokens[4].text != "model" || tokens.back().text != "endmodel")
|
||||||
|
{
|
||||||
|
Error = "unexpected layout of model definition";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<text_change> changes;
|
||||||
|
auto const replace = [&](std::size_t const Index, std::string const &Value) { changes.push_back({tokens[Index].begin, tokens[Index].end, Value}); };
|
||||||
|
// optional blocks of three values, located past the texture
|
||||||
|
auto const last{tokens.size() - 1};
|
||||||
|
auto const find_block = [&](char const *Keyword) {
|
||||||
|
for (std::size_t idx = 11; idx + 3 < last; ++idx)
|
||||||
|
{
|
||||||
|
if (tokens[idx].text == Keyword)
|
||||||
|
{
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::size_t{0};
|
||||||
|
};
|
||||||
|
std::string additions; // blocks to insert ahead of the closing keyword
|
||||||
|
|
||||||
|
if (Placement.location)
|
||||||
|
{
|
||||||
|
replace(5, number(Placement.location->x));
|
||||||
|
replace(6, number(Placement.location->y));
|
||||||
|
replace(7, number(Placement.location->z));
|
||||||
|
}
|
||||||
|
if (Placement.angles)
|
||||||
|
{
|
||||||
|
auto const &angles{*Placement.angles};
|
||||||
|
auto const block{find_block("angles")};
|
||||||
|
if (block != 0)
|
||||||
|
{
|
||||||
|
// the block specifies complete rotation of the instance, regardless of anything else
|
||||||
|
replace(block + 1, number(angles.x));
|
||||||
|
replace(block + 2, number(angles.y));
|
||||||
|
replace(block + 3, number(angles.z));
|
||||||
|
}
|
||||||
|
else if (about_equal(angles.x, Placement.rotation.x) && about_equal(angles.z, Placement.rotation.z))
|
||||||
|
{
|
||||||
|
// rotation around the vertical axis alone fits in the basic angle parameter
|
||||||
|
replace(8, number(angles.y - Placement.rotation.y));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
additions += "angles " + numbers(angles) + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Placement.scale)
|
||||||
|
{
|
||||||
|
auto const &scale{*Placement.scale};
|
||||||
|
auto const unit{about_equal(scale.x, 1.f) && about_equal(scale.y, 1.f) && about_equal(scale.z, 1.f)};
|
||||||
|
auto const block{find_block("scale")};
|
||||||
|
if (block != 0)
|
||||||
|
{
|
||||||
|
if (unit)
|
||||||
|
{
|
||||||
|
// drop the block along with the whitespace separating it from the next token
|
||||||
|
changes.push_back({tokens[block].begin, tokens[block + 4].begin, ""});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
replace(block + 1, number(scale.x));
|
||||||
|
replace(block + 2, number(scale.y));
|
||||||
|
replace(block + 3, number(scale.z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (false == unit)
|
||||||
|
{
|
||||||
|
additions += "scale " + numbers(scale) + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (false == additions.empty())
|
||||||
|
{
|
||||||
|
changes.push_back({tokens[last].begin, tokens[last].begin, additions});
|
||||||
|
}
|
||||||
|
apply(Text, changes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// definition layout: node <max> <min> <name> memcell <x> <y> <z> <text> <value 1> <value 2> <track> endmemcell
|
||||||
|
bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &Error)
|
||||||
|
{
|
||||||
|
auto const tokens{tokenize(Text)};
|
||||||
|
if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell")
|
||||||
|
{
|
||||||
|
Error = "unexpected layout of memory cell definition";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string detect_eol(std::string const &Text, std::string const &Fallback)
|
||||||
|
{
|
||||||
|
auto const linebreak{Text.find('\n')};
|
||||||
|
if (linebreak == std::string::npos)
|
||||||
|
{
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
return (linebreak > 0 && Text[linebreak - 1] == '\r') ? "\r\n" : "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensure_newline(std::string &Text, std::string const &Eol)
|
||||||
|
{
|
||||||
|
if (false == Text.empty() && Text.back() != '\n')
|
||||||
|
{
|
||||||
|
Text += Eol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_file(std::string const &Path, std::string &Content)
|
||||||
|
{
|
||||||
|
std::ifstream input{Path, std::ios_base::binary};
|
||||||
|
if (false == input.is_open())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Content.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// changes to make in the text of a single scenery file
|
||||||
|
struct node_layers::file_patch
|
||||||
|
{
|
||||||
|
struct edit
|
||||||
|
{
|
||||||
|
std::streamoff begin{0}; // replaced range of the original text. empty range inserts, negative location stands for the end of the text
|
||||||
|
std::streamoff end{0};
|
||||||
|
std::string text; // replacement
|
||||||
|
std::size_t lead{0}; // size of the part of replacement preceding the definition it carries
|
||||||
|
std::size_t length{0}; // size of that definition
|
||||||
|
basic_node const *node{nullptr}; // node defined by the replacement
|
||||||
|
layer_handle site{null_handle}; // layer included by the directive in the replacement
|
||||||
|
layer_handle inlined{null_handle}; // layer whose content replaces the range
|
||||||
|
};
|
||||||
|
std::vector<edit> edits;
|
||||||
|
};
|
||||||
|
|
||||||
|
// text of a scenery file after the changes, with the new locations of what it defines
|
||||||
|
struct node_layers::composition
|
||||||
|
{
|
||||||
|
struct node_place
|
||||||
|
{
|
||||||
|
basic_node const *node;
|
||||||
|
source_span span;
|
||||||
|
bool rewritten;
|
||||||
|
};
|
||||||
|
struct site_place
|
||||||
|
{
|
||||||
|
layer_handle layer;
|
||||||
|
source_span span;
|
||||||
|
};
|
||||||
|
std::string text;
|
||||||
|
std::vector<node_place> nodes;
|
||||||
|
std::vector<site_place> sites;
|
||||||
|
|
||||||
|
// adds another piece of text at the end
|
||||||
|
void append(composition const &Other)
|
||||||
|
{
|
||||||
|
auto const offset{static_cast<std::streamoff>(text.size())};
|
||||||
|
text += Other.text;
|
||||||
|
for (auto place : Other.nodes)
|
||||||
|
{
|
||||||
|
place.span.begin += offset;
|
||||||
|
place.span.end += offset;
|
||||||
|
nodes.emplace_back(place);
|
||||||
|
}
|
||||||
|
for (auto place : Other.sites)
|
||||||
|
{
|
||||||
|
place.span.begin += offset;
|
||||||
|
place.span.end += offset;
|
||||||
|
sites.emplace_back(place);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct node_layers::save_state
|
||||||
|
{
|
||||||
|
std::map<layer_handle, std::string> content; // original text of the files
|
||||||
|
std::map<layer_handle, file_patch> patches;
|
||||||
|
std::map<layer_handle, std::vector<basic_node const *>> nodes; // unchanged nodes defined in each file
|
||||||
|
std::map<layer_handle, std::vector<layer_handle>> includes; // unchanged include directives in each file, by included layer
|
||||||
|
std::string eol{"\r\n"};
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// produces text of specified file with the changes applied
|
||||||
|
bool node_layers::compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const
|
||||||
|
{
|
||||||
|
if (Depth > 32)
|
||||||
|
{
|
||||||
|
State.error = "layers include each other in a loop";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static std::string const empty;
|
||||||
|
auto const contentlookup{State.content.find(Layer)};
|
||||||
|
auto const &base{contentlookup != State.content.end() ? contentlookup->second : empty};
|
||||||
|
auto const basesize{static_cast<std::streamoff>(base.size())};
|
||||||
|
auto const eol{detect_eol(base, State.eol)};
|
||||||
|
|
||||||
|
std::vector<file_patch::edit> edits;
|
||||||
|
if (auto const lookup{State.patches.find(Layer)}; lookup != State.patches.end())
|
||||||
|
{
|
||||||
|
edits = lookup->second.edits;
|
||||||
|
}
|
||||||
|
for (auto &edit : edits)
|
||||||
|
{
|
||||||
|
if (edit.begin < 0)
|
||||||
|
{
|
||||||
|
edit.begin = edit.end = basesize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::stable_sort(std::begin(edits), std::end(edits), [](file_patch::edit const &Left, file_patch::edit const &Right) { return Left.begin < Right.begin; });
|
||||||
|
std::streamoff cursor{0};
|
||||||
|
for (auto const &edit : edits)
|
||||||
|
{
|
||||||
|
if (edit.begin < cursor || edit.end < edit.begin || edit.end > basesize)
|
||||||
|
{
|
||||||
|
State.error = "conflicting changes in file \"" + layer(Layer).name + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cursor = edit.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// locations in the original text past which everything is shifted by given amount
|
||||||
|
std::vector<std::pair<std::streamoff, std::streamoff>> shifts;
|
||||||
|
cursor = 0;
|
||||||
|
for (std::size_t idx = 0; idx < edits.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const &edit{edits[idx]};
|
||||||
|
auto begin{edit.begin};
|
||||||
|
auto end{edit.end};
|
||||||
|
if (edit.text.empty() && edit.inlined == null_handle && end > begin)
|
||||||
|
{
|
||||||
|
// a definition is removed. if it had a line for itself take the whole line out, instead of leaving it blank
|
||||||
|
auto const limit{idx + 1 < edits.size() ? edits[idx + 1].begin : basesize};
|
||||||
|
auto linebegin{begin};
|
||||||
|
while (linebegin > cursor && (base[linebegin - 1] == ' ' || base[linebegin - 1] == '\t'))
|
||||||
|
{
|
||||||
|
--linebegin;
|
||||||
|
}
|
||||||
|
auto lineend{end};
|
||||||
|
while (lineend < limit && (base[lineend] == ' ' || base[lineend] == '\t'))
|
||||||
|
{
|
||||||
|
++lineend;
|
||||||
|
}
|
||||||
|
auto const startsline{linebegin == 0 || base[linebegin - 1] == '\n'};
|
||||||
|
auto endsline{lineend == basesize};
|
||||||
|
if (lineend + 1 < limit && base[lineend] == '\r' && base[lineend + 1] == '\n')
|
||||||
|
{
|
||||||
|
lineend += 2;
|
||||||
|
endsline = true;
|
||||||
|
}
|
||||||
|
else if (lineend < limit && base[lineend] == '\n')
|
||||||
|
{
|
||||||
|
lineend += 1;
|
||||||
|
endsline = true;
|
||||||
|
}
|
||||||
|
if (startsline && endsline)
|
||||||
|
{
|
||||||
|
begin = linebegin;
|
||||||
|
end = lineend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Output.text.append(base, static_cast<std::size_t>(cursor), static_cast<std::size_t>(begin - cursor));
|
||||||
|
if (edit.inlined != null_handle)
|
||||||
|
{
|
||||||
|
// content of the included file takes place of the include directive
|
||||||
|
composition included;
|
||||||
|
if (false == compose(State, edit.inlined, included, Depth + 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (end < basesize && (base[end] == '\r' || base[end] == '\n'))
|
||||||
|
{
|
||||||
|
// the line break which followed the directive ends the included text now, a second one would leave a blank line
|
||||||
|
if (false == included.text.empty() && included.text.back() == '\n')
|
||||||
|
{
|
||||||
|
included.text.pop_back();
|
||||||
|
if (false == included.text.empty() && included.text.back() == '\r')
|
||||||
|
{
|
||||||
|
included.text.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ensure_newline(included.text, eol);
|
||||||
|
}
|
||||||
|
Output.append(included);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the line break leading the replacement isn't needed if the text ahead happens to end with one
|
||||||
|
auto const lead{(Output.text.empty() || Output.text.back() == '\n') ? std::size_t{0} : edit.lead};
|
||||||
|
auto const offset{static_cast<std::streamoff>(Output.text.size() + lead)};
|
||||||
|
auto const span{source_span{offset, offset + static_cast<std::streamoff>(edit.length)}};
|
||||||
|
if (edit.node != nullptr)
|
||||||
|
{
|
||||||
|
Output.nodes.push_back({edit.node, span, true});
|
||||||
|
}
|
||||||
|
if (edit.site != null_handle)
|
||||||
|
{
|
||||||
|
Output.sites.push_back({edit.site, span});
|
||||||
|
}
|
||||||
|
Output.text.append(edit.text, edit.lead - lead, std::string::npos);
|
||||||
|
}
|
||||||
|
cursor = end;
|
||||||
|
shifts.emplace_back(end, static_cast<std::streamoff>(Output.text.size()) - end);
|
||||||
|
}
|
||||||
|
Output.text.append(base, static_cast<std::size_t>(cursor), std::string::npos);
|
||||||
|
|
||||||
|
// what the changes didn't touch only moves along with the text around it
|
||||||
|
auto const shift = [&](source_span Span) {
|
||||||
|
auto const lookup{std::upper_bound(std::begin(shifts), std::end(shifts), Span.begin,
|
||||||
|
[](std::streamoff const Location, std::pair<std::streamoff, std::streamoff> const &Shift) { return Location < Shift.first; })};
|
||||||
|
auto const offset{lookup == std::begin(shifts) ? 0 : std::prev(lookup)->second};
|
||||||
|
Span.begin += offset;
|
||||||
|
Span.end += offset;
|
||||||
|
return Span;
|
||||||
|
};
|
||||||
|
if (auto const lookup{State.nodes.find(Layer)}; lookup != State.nodes.end())
|
||||||
|
{
|
||||||
|
for (auto const *node : lookup->second)
|
||||||
|
{
|
||||||
|
Output.nodes.push_back({node, shift(m_sources.at(node).span), false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto const lookup{State.includes.find(Layer)}; lookup != State.includes.end())
|
||||||
|
{
|
||||||
|
for (auto const included : lookup->second)
|
||||||
|
{
|
||||||
|
Output.sites.push_back({included, shift(layer(included).sites.front().span)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// writes changes made in the editor to the scenery files
|
||||||
|
save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||||
|
{
|
||||||
|
save_result result;
|
||||||
|
if (m_layers.empty())
|
||||||
|
{
|
||||||
|
result.message = "The scenery wasn't opened for editing, there are no layers to save.";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
auto const root{layer_handle{1}};
|
||||||
|
save_state state;
|
||||||
|
auto const fail = [&](std::string const &Message) {
|
||||||
|
result.message = "Scenery not saved: " + Message;
|
||||||
|
ErrorLog(result.message);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
// loads text of specified layer file, if it wasn't loaded yet
|
||||||
|
auto const load = [&](layer_handle const Layer) {
|
||||||
|
if (layer(Layer).created || state.content.count(Layer) != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::error_code error;
|
||||||
|
auto const filepath{std::filesystem::path(path(Layer))};
|
||||||
|
auto const filesize{std::filesystem::file_size(filepath, error)};
|
||||||
|
auto const filetime{error ? std::filesystem::file_time_type{} : std::filesystem::last_write_time(filepath, error)};
|
||||||
|
std::string content;
|
||||||
|
if (error || false == read_file(path(Layer), content))
|
||||||
|
{
|
||||||
|
state.error = "can't read file \"" + layer(Layer).name + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filesize != layer(Layer).filesize || filetime != layer(Layer).filetime || content.size() != filesize)
|
||||||
|
{
|
||||||
|
// locations of the definitions are no longer valid
|
||||||
|
state.error = "file \"" + layer(Layer).name + "\" was changed outside of the editor since the scenery was loaded";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.content.emplace(Layer, std::move(content));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// a layer is saved if it's still a part of the scenery, as a file of its own
|
||||||
|
auto const is_output = [&](layer_handle const Layer) { return listed(Layer) && false == layer(Layer).removed && false == layer(Layer).binary; };
|
||||||
|
|
||||||
|
if (false == load(root))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
state.eol = detect_eol(state.content[root], "\r\n");
|
||||||
|
|
||||||
|
// changed and deleted nodes
|
||||||
|
std::set<basic_node const *> rewritten;
|
||||||
|
std::map<layer_handle, std::vector<std::pair<basic_node const *, std::string>>> created; // new definitions, by target layer
|
||||||
|
auto const process = [&](basic_node const *Node, TAnimModel const *Model) {
|
||||||
|
auto const nodelayer{resolve(Node->layer())};
|
||||||
|
if (false == is_output(nodelayer))
|
||||||
|
{
|
||||||
|
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}};
|
||||||
|
auto const lookup{m_sources.find(Node)};
|
||||||
|
std::string error;
|
||||||
|
if (lookup != m_sources.end())
|
||||||
|
{
|
||||||
|
// defined in a scenery file. rewrite the placement in its definition if it's no longer the one it was loaded with
|
||||||
|
auto const &source{lookup->second};
|
||||||
|
if (false == writable(source.layer))
|
||||||
|
{
|
||||||
|
// the editor doesn't let these be changed, as one definition stands for more than one node
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto const moved{glm::distance(location, source.location) > 1e-4};
|
||||||
|
auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))};
|
||||||
|
auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))};
|
||||||
|
if (false == (moved || rotated || scaled))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (false == load(source.layer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const &content{state.content[source.layer]};
|
||||||
|
if (source.span.end > static_cast<std::streamoff>(content.size()))
|
||||||
|
{
|
||||||
|
state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto text{content.substr(static_cast<std::size_t>(source.span.begin), static_cast<std::size_t>(source.span.end - source.span.begin))};
|
||||||
|
auto patched{false};
|
||||||
|
if (Model != nullptr)
|
||||||
|
{
|
||||||
|
model_placement placement;
|
||||||
|
placement.rotation = source.context.rotation;
|
||||||
|
if (moved)
|
||||||
|
{
|
||||||
|
placement.location = source.context.to_local(location);
|
||||||
|
}
|
||||||
|
if (rotated)
|
||||||
|
{
|
||||||
|
placement.angles = angles;
|
||||||
|
}
|
||||||
|
if (scaled)
|
||||||
|
{
|
||||||
|
placement.scale = scale / source.context.scale;
|
||||||
|
}
|
||||||
|
patched = patch_model(text, placement, error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
patched = patch_memcell(text, source.context.to_local(location), error);
|
||||||
|
}
|
||||||
|
if (false == patched)
|
||||||
|
{
|
||||||
|
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file_patch::edit edit;
|
||||||
|
edit.begin = source.span.begin;
|
||||||
|
edit.end = source.span.end;
|
||||||
|
edit.length = text.size();
|
||||||
|
edit.text = std::move(text);
|
||||||
|
edit.node = Node;
|
||||||
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
||||||
|
rewritten.emplace(Node);
|
||||||
|
}
|
||||||
|
else if (Model != nullptr && false == Node->m_template)
|
||||||
|
{
|
||||||
|
// created in the editor. the definition is placed at the end of the layer file, with the placement in effect there
|
||||||
|
std::string text;
|
||||||
|
Node->export_as_text(text);
|
||||||
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
||||||
|
auto const &context{layer(nodelayer).context_end};
|
||||||
|
if (false == context.matches(layer_context()))
|
||||||
|
{
|
||||||
|
model_placement placement;
|
||||||
|
placement.rotation = context.rotation;
|
||||||
|
placement.location = context.to_local(location);
|
||||||
|
placement.angles = angles;
|
||||||
|
placement.scale = scale / context.scale;
|
||||||
|
if (false == patch_model(text, placement, error))
|
||||||
|
{
|
||||||
|
state.error = error + " of \"" + Node->name() + "\"";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
created[nodelayer].emplace_back(Node, std::move(text));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
for (auto const *instance : simulation::Instances.sequence())
|
||||||
|
{
|
||||||
|
if (instance != nullptr && false == process(instance, instance))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto const *memorycell : simulation::Memory.sequence())
|
||||||
|
{
|
||||||
|
if (memorycell != nullptr && false == process(memorycell, nullptr))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto const &erased : m_erased)
|
||||||
|
{
|
||||||
|
file_patch::edit edit;
|
||||||
|
edit.begin = erased.second.begin;
|
||||||
|
edit.end = erased.second.end;
|
||||||
|
state.patches[erased.first].edits.emplace_back(std::move(edit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// layer changes
|
||||||
|
std::map<layer_handle, std::vector<layer_handle>> appended; // merged layers placed at the end of their target
|
||||||
|
std::set<layer_handle> absorbed; // merged layers which cease to exist as files
|
||||||
|
std::set<layer_handle> detached; // layers whose include directives are replaced
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
auto const &source{layer(candidate)};
|
||||||
|
if (source.dead)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const hassite{false == source.created && source.sites.size() == 1 && false == source.sites.front().fixed && source.sites.front().span.valid()};
|
||||||
|
if (source.merged != null_handle)
|
||||||
|
{
|
||||||
|
auto const target{resolve(candidate)};
|
||||||
|
absorbed.emplace(candidate);
|
||||||
|
if (hassite)
|
||||||
|
{
|
||||||
|
file_patch::edit edit;
|
||||||
|
edit.begin = source.sites.front().span.begin;
|
||||||
|
edit.end = source.sites.front().span.end;
|
||||||
|
if (resolve(source.sites.front().parent) == target)
|
||||||
|
{
|
||||||
|
edit.inlined = candidate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appended[target].emplace_back(candidate);
|
||||||
|
}
|
||||||
|
state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit));
|
||||||
|
detached.emplace(candidate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appended[target].emplace_back(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (source.removed)
|
||||||
|
{
|
||||||
|
if (hassite)
|
||||||
|
{
|
||||||
|
file_patch::edit edit;
|
||||||
|
edit.begin = source.sites.front().span.begin;
|
||||||
|
edit.end = source.sites.front().span.end;
|
||||||
|
state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit));
|
||||||
|
detached.emplace(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (source.created)
|
||||||
|
{
|
||||||
|
// include directive for the new file, on a line of its own
|
||||||
|
auto const parent{source.parent};
|
||||||
|
auto const directive{"include " + source.name + " end"};
|
||||||
|
file_patch::edit edit;
|
||||||
|
edit.site = candidate;
|
||||||
|
edit.length = directive.size();
|
||||||
|
if (valid(source.anchor) && layer(source.anchor).sites.size() == 1 && layer(source.anchor).sites.front().parent == parent && layer(source.anchor).sites.front().span.valid())
|
||||||
|
{
|
||||||
|
edit.begin = edit.end = layer(source.anchor).sites.front().span.end;
|
||||||
|
edit.text = state.eol + directive;
|
||||||
|
edit.lead = state.eol.size();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
edit.begin = edit.end = -1;
|
||||||
|
if (false == layer(parent).created)
|
||||||
|
{
|
||||||
|
if (false == load(parent))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
auto const &content{state.content[parent]};
|
||||||
|
if (false == content.empty() && content.back() != '\n')
|
||||||
|
{
|
||||||
|
edit.text = state.eol;
|
||||||
|
edit.lead = state.eol.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edit.text += directive + state.eol;
|
||||||
|
}
|
||||||
|
state.patches[parent].edits.emplace_back(std::move(edit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// files to write
|
||||||
|
std::vector<layer_handle> outputs;
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
if (false == is_output(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 ||
|
||||||
|
(candidate == root && false == Rootstatements.empty()))
|
||||||
|
{
|
||||||
|
outputs.emplace_back(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// text of the merged layers travels with the file they end up in, but changes made to it are still theirs
|
||||||
|
std::vector<layer_handle> composed{outputs};
|
||||||
|
composed.insert(std::end(composed), std::begin(absorbed), std::end(absorbed));
|
||||||
|
for (auto const candidate : composed)
|
||||||
|
{
|
||||||
|
if (false == load(candidate))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// what is defined in these files and isn't rewritten moves along with the text
|
||||||
|
for (auto const &source : m_sources)
|
||||||
|
{
|
||||||
|
if (state.content.count(source.second.layer) != 0 && rewritten.count(source.first) == 0)
|
||||||
|
{
|
||||||
|
state.nodes[source.second.layer].emplace_back(source.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
auto const &included{layer(candidate)};
|
||||||
|
if (included.dead || included.created || detached.count(candidate) != 0 || included.sites.size() != 1 || included.sites.front().fixed || false == included.sites.front().span.valid())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
state.includes[included.sites.front().parent].emplace_back(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<layer_handle, composition> texts;
|
||||||
|
for (auto const output : outputs)
|
||||||
|
{
|
||||||
|
auto &text{texts[output]};
|
||||||
|
auto const eol{detect_eol(state.content.count(output) != 0 ? state.content[output] : std::string{}, state.eol)};
|
||||||
|
if (layer(output).created)
|
||||||
|
{
|
||||||
|
// NOTE: the parser treats an empty file as a missing one
|
||||||
|
text.text = "// scenery layer created in the editor" + eol;
|
||||||
|
}
|
||||||
|
composition body;
|
||||||
|
if (false == compose(state, output, body, 0))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
text.append(body);
|
||||||
|
if (auto const lookup{appended.find(output)}; lookup != appended.end())
|
||||||
|
{
|
||||||
|
for (auto const merged : lookup->second)
|
||||||
|
{
|
||||||
|
composition mergedtext;
|
||||||
|
if (false == compose(state, merged, mergedtext, 0))
|
||||||
|
{
|
||||||
|
return fail(state.error);
|
||||||
|
}
|
||||||
|
ensure_newline(text.text, eol);
|
||||||
|
text.append(mergedtext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto const lookup{created.find(output)}; lookup != created.end())
|
||||||
|
{
|
||||||
|
for (auto const &definition : lookup->second)
|
||||||
|
{
|
||||||
|
ensure_newline(text.text, eol);
|
||||||
|
auto const offset{static_cast<std::streamoff>(text.text.size())};
|
||||||
|
text.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
|
||||||
|
text.text += definition.second + eol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (output == root)
|
||||||
|
{
|
||||||
|
for (auto const &statement : Rootstatements)
|
||||||
|
{
|
||||||
|
ensure_newline(text.text, eol);
|
||||||
|
text.text += statement + eol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// with everything worked out it's safe to touch the files. new content goes to temporary files first,
|
||||||
|
// so a failure half way through doesn't leave the scenery with only a part of the changes
|
||||||
|
std::error_code error;
|
||||||
|
std::vector<layer_handle> written;
|
||||||
|
for (auto const output : outputs)
|
||||||
|
{
|
||||||
|
auto const lookup{state.content.find(output)};
|
||||||
|
if (lookup != state.content.end() && lookup->second == texts[output].text)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const filepath{std::filesystem::path(path(output))};
|
||||||
|
if (filepath.has_parent_path())
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(filepath.parent_path(), error);
|
||||||
|
}
|
||||||
|
std::ofstream file{path(output) + ".tmp", std::ios_base::binary | std::ios_base::trunc};
|
||||||
|
file << texts[output].text;
|
||||||
|
file.close();
|
||||||
|
if (file.fail())
|
||||||
|
{
|
||||||
|
for (auto const leftover : written)
|
||||||
|
{
|
||||||
|
std::filesystem::remove(path(leftover) + ".tmp", error);
|
||||||
|
}
|
||||||
|
std::filesystem::remove(path(output) + ".tmp", error);
|
||||||
|
return fail("can't write file \"" + layer(output).name + "\"");
|
||||||
|
}
|
||||||
|
written.emplace_back(output);
|
||||||
|
}
|
||||||
|
for (auto const output : written)
|
||||||
|
{
|
||||||
|
auto const filepath{path(output)};
|
||||||
|
// the first save of a file leaves its original content next to it
|
||||||
|
if (false == layer(output).editormade && std::filesystem::exists(filepath, error) && false == std::filesystem::exists(filepath + ".bak", error))
|
||||||
|
{
|
||||||
|
std::filesystem::copy_file(filepath, filepath + ".bak", error);
|
||||||
|
}
|
||||||
|
std::filesystem::rename(filepath + ".tmp", filepath, error);
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
return fail("can't replace file \"" + layer(output).name + "\": " + error.message());
|
||||||
|
}
|
||||||
|
result.files.emplace_back(layer(output).name);
|
||||||
|
WriteLog("Scenery save: file \"" + layer(output).name + "\" updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// the scenery files now match the scene, update the bookkeeping to reflect it
|
||||||
|
auto removedterrain{false};
|
||||||
|
for (auto const output : outputs)
|
||||||
|
{
|
||||||
|
for (auto const &place : texts[output].nodes)
|
||||||
|
{
|
||||||
|
auto &source{m_sources[place.node]};
|
||||||
|
auto const isnew{source.layer == null_handle};
|
||||||
|
source.layer = output;
|
||||||
|
source.span = place.span;
|
||||||
|
if (isnew)
|
||||||
|
{
|
||||||
|
source.context = layer(output).context_end;
|
||||||
|
}
|
||||||
|
if (isnew || place.rewritten)
|
||||||
|
{
|
||||||
|
auto const *model{dynamic_cast<TAnimModel const *>(place.node)};
|
||||||
|
source.location = place.node->location();
|
||||||
|
source.angles = (model != nullptr ? model->Angles() : glm::vec3{0.f});
|
||||||
|
source.scale = (model != nullptr ? model->Scale() : glm::vec3{1.f});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto const &place : texts[output].sites)
|
||||||
|
{
|
||||||
|
auto &included{layer(place.layer)};
|
||||||
|
if (included.sites.empty())
|
||||||
|
{
|
||||||
|
// directive of a layer created in the editor
|
||||||
|
included.sites.emplace_back();
|
||||||
|
}
|
||||||
|
included.sites.front().parent = output;
|
||||||
|
included.sites.front().span = place.span;
|
||||||
|
included.parent = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
|
||||||
|
{
|
||||||
|
auto const candidate{static_cast<layer_handle>(idx + 1)};
|
||||||
|
auto &saved{layer(candidate)};
|
||||||
|
if (saved.dead)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (saved.merged != null_handle || saved.removed)
|
||||||
|
{
|
||||||
|
removedterrain |= saved.removed && (saved.items[static_cast<std::size_t>(layer_item::shape)] + saved.items[static_cast<std::size_t>(layer_item::lines)] > 0);
|
||||||
|
saved.dead = true;
|
||||||
|
saved.sites.clear();
|
||||||
|
}
|
||||||
|
saved.created = false;
|
||||||
|
saved.anchor = null_handle;
|
||||||
|
}
|
||||||
|
for (auto const output : written)
|
||||||
|
{
|
||||||
|
stat(output);
|
||||||
|
}
|
||||||
|
m_erased.clear();
|
||||||
|
if (removedterrain)
|
||||||
|
{
|
||||||
|
// binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load
|
||||||
|
auto terrainfile{layer(root).name};
|
||||||
|
while (false == terrainfile.empty() && terrainfile.front() == '$')
|
||||||
|
{
|
||||||
|
terrainfile.erase(0, 1);
|
||||||
|
}
|
||||||
|
erase_extension(terrainfile);
|
||||||
|
if (std::filesystem::remove(Global.asCurrentSceneryPath + terrainfile + ".sbt", error))
|
||||||
|
{
|
||||||
|
WriteLog("Scenery save: binary terrain file removed, it'll be generated again on the next load");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
if (result.files.empty())
|
||||||
|
{
|
||||||
|
result.message = "No changes to save.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.message = "Saved: ";
|
||||||
|
for (auto const &file : result.files)
|
||||||
|
{
|
||||||
|
result.message += file + (&file != &result.files.back() ? ", " : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scene
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
@@ -718,6 +718,7 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) {
|
|||||||
|
|
||||||
basic_node::basic_node( scene::node_data const &Nodedata ) :
|
basic_node::basic_node( scene::node_data const &Nodedata ) :
|
||||||
m_name( Nodedata.name ),
|
m_name( Nodedata.name ),
|
||||||
|
m_template( Nodedata.is_template ),
|
||||||
m_layer( Nodedata.layer )
|
m_layer( Nodedata.layer )
|
||||||
{
|
{
|
||||||
uuid = UID::random();
|
uuid = UID::random();
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ struct node_data {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string type;
|
std::string type;
|
||||||
layer_handle layer { null_handle }; // scenery layer the node is defined in, if any
|
layer_handle layer { null_handle }; // scenery layer the node is defined in, if any
|
||||||
|
bool is_template { false }; // the node is defined by an *.inc template rather than directly by a scenery layer file
|
||||||
};
|
};
|
||||||
|
|
||||||
// holds unique piece of geometry, covered with single material
|
// holds unique piece of geometry, covered with single material
|
||||||
@@ -381,6 +382,7 @@ public:
|
|||||||
std::string m_name;
|
std::string m_name;
|
||||||
bool m_dirty { false };
|
bool m_dirty { false };
|
||||||
bool m_layerhidden { false }; // the node was visible until its scenery layer was hidden
|
bool m_layerhidden { false }; // the node was visible until its scenery layer was hidden
|
||||||
|
bool m_template { false }; // the node is defined by an *.inc template, which the editor can't rewrite. set only for scenery opened for editing
|
||||||
scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any
|
scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any
|
||||||
UID uuid;
|
UID uuid;
|
||||||
|
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ TEventLauncher * state_manager::create_eventlauncher(const std::string &src, con
|
|||||||
|
|
||||||
void state_manager::delete_model(TAnimModel *model) {
|
void state_manager::delete_model(TAnimModel *model) {
|
||||||
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
|
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
|
||||||
|
scene::Layers.forget(model);
|
||||||
Region->erase(model);
|
Region->erase(model);
|
||||||
Instances.purge(model);
|
Instances.purge(model);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,8 +437,11 @@ void
|
|||||||
state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ) {
|
state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ) {
|
||||||
|
|
||||||
auto const inputline = Input.Line(); // cache in case we need to report error
|
auto const inputline = Input.Line(); // cache in case we need to report error
|
||||||
|
auto const sourcebegin = Input.TokenBegin(); // location of the node definition, for scenery opened for editing
|
||||||
|
|
||||||
scene::node_data nodedata;
|
scene::node_data nodedata;
|
||||||
|
nodedata.layer = scene::Layers.handle();
|
||||||
|
nodedata.is_template = ( nodedata.layer != null_handle ) && ( false == Input.InLayerFile() );
|
||||||
// common data and node type indicator
|
// common data and node type indicator
|
||||||
Input.getTokens( 4 );
|
Input.getTokens( 4 );
|
||||||
Input
|
Input
|
||||||
@@ -447,7 +450,6 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
|||||||
>> nodedata.name
|
>> nodedata.name
|
||||||
>> nodedata.type;
|
>> nodedata.type;
|
||||||
if( nodedata.name == "none" ) { nodedata.name.clear(); }
|
if( nodedata.name == "none" ) { nodedata.name.clear(); }
|
||||||
nodedata.layer = scene::Layers.handle();
|
|
||||||
// type-based deserialization. not elegant but it'll do
|
// type-based deserialization. not elegant but it'll do
|
||||||
if( nodedata.type == "dynamic" ) {
|
if( nodedata.type == "dynamic" ) {
|
||||||
|
|
||||||
@@ -571,6 +573,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
|||||||
}
|
}
|
||||||
scene::Groups.insert( scene::Groups.handle(), instance );
|
scene::Groups.insert( scene::Groups.handle(), instance );
|
||||||
simulation::Region->insert( instance );
|
simulation::Region->insert( instance );
|
||||||
|
scene::Layers.track( instance, { sourcebegin, Input.TokenEnd() }, instance->Angles(), instance->Scale() );
|
||||||
scene::basic_node *hierarchy_node = instance;
|
scene::basic_node *hierarchy_node = instance;
|
||||||
if (hierarchy_node)
|
if (hierarchy_node)
|
||||||
{ scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node;
|
{ scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node;
|
||||||
@@ -626,6 +629,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
|||||||
}
|
}
|
||||||
scene::Groups.insert( scene::Groups.handle(), memorycell );
|
scene::Groups.insert( scene::Groups.handle(), memorycell );
|
||||||
simulation::Region->insert( memorycell );
|
simulation::Region->insert( memorycell );
|
||||||
|
scene::Layers.track( memorycell, { sourcebegin, Input.TokenEnd() } );
|
||||||
}
|
}
|
||||||
else if( nodedata.type == "eventlauncher" ) {
|
else if( nodedata.type == "eventlauncher" ) {
|
||||||
|
|
||||||
@@ -685,6 +689,22 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// passes the placement in effect to the layer bookkeeping of scenery opened for editing
|
||||||
|
void
|
||||||
|
sync_layer_context( scene::scratch_data const &Scratchpad ) {
|
||||||
|
|
||||||
|
if( true == scene::Layers.empty() ) { return; }
|
||||||
|
|
||||||
|
scene::Layers.context( {
|
||||||
|
( Scratchpad.location.offset.empty() ? glm::dvec3{ 0.0 } : Scratchpad.location.offset.top() ),
|
||||||
|
Scratchpad.location.rotation,
|
||||||
|
( Scratchpad.location.scale.empty() ? glm::vec3{ 1.f } : Scratchpad.location.scale.top() ) } );
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void
|
void
|
||||||
state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ) {
|
state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ) {
|
||||||
|
|
||||||
@@ -700,6 +720,7 @@ state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scrat
|
|||||||
Scratchpad.location.offset.empty() ?
|
Scratchpad.location.offset.empty() ?
|
||||||
glm::dvec3() :
|
glm::dvec3() :
|
||||||
Scratchpad.location.offset.top() ) );
|
Scratchpad.location.offset.top() ) );
|
||||||
|
sync_layer_context( Scratchpad );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -711,6 +732,7 @@ state_serializer::deserialize_endorigin( cParser &Input, scene::scratch_data &Sc
|
|||||||
else {
|
else {
|
||||||
ErrorLog( "Bad origin: endorigin instruction with empty origin stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
|
ErrorLog( "Bad origin: endorigin instruction with empty origin stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
|
||||||
}
|
}
|
||||||
|
sync_layer_context( Scratchpad );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -734,6 +756,7 @@ state_serializer::deserialize_scale( cParser &Input, scene::scratch_data &Scratc
|
|||||||
// scales compose component-wise, mirroring how origin offsets compose additively.
|
// scales compose component-wise, mirroring how origin offsets compose additively.
|
||||||
glm::vec3 const parent = Scratchpad.location.scale.empty() ? glm::vec3(1.0f) : Scratchpad.location.scale.top();
|
glm::vec3 const parent = Scratchpad.location.scale.empty() ? glm::vec3(1.0f) : Scratchpad.location.scale.top();
|
||||||
Scratchpad.location.scale.emplace( factor * parent );
|
Scratchpad.location.scale.emplace( factor * parent );
|
||||||
|
sync_layer_context( Scratchpad );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -745,6 +768,7 @@ state_serializer::deserialize_endscale( cParser &Input, scene::scratch_data &Scr
|
|||||||
else {
|
else {
|
||||||
ErrorLog( "Bad scale: endscale instruction with empty scale stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
|
ErrorLog( "Bad scale: endscale instruction with empty scale stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
|
||||||
}
|
}
|
||||||
|
sync_layer_context( Scratchpad );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -755,6 +779,7 @@ state_serializer::deserialize_rotate( cParser &Input, scene::scratch_data &Scrat
|
|||||||
>> Scratchpad.location.rotation.x
|
>> Scratchpad.location.rotation.x
|
||||||
>> Scratchpad.location.rotation.y
|
>> Scratchpad.location.rotation.y
|
||||||
>> Scratchpad.location.rotation.z;
|
>> Scratchpad.location.rotation.z;
|
||||||
|
sync_layer_context( Scratchpad );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -860,6 +885,7 @@ state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data
|
|||||||
Input.getTokens(4);
|
Input.getTokens(4);
|
||||||
Input >> folder >> cells >> cellsize >> radius;
|
Input >> folder >> cells >> cellsize >> radius;
|
||||||
skip_until(Input, "endeditorterrain");
|
skip_until(Input, "endeditorterrain");
|
||||||
|
scene::Layers.terrain_directive(true);
|
||||||
|
|
||||||
if (!folder.empty() && cells > 0 && cellsize > 0.0f)
|
if (!folder.empty() && cells > 0 && cellsize > 0.0f)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -237,11 +237,13 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
|
|||||||
|
|
||||||
const auto breakTable = makeBreakTable(Break);
|
const auto breakTable = makeBreakTable(Break);
|
||||||
char c = 0;
|
char c = 0;
|
||||||
|
bool separated = false; // the token was ended by a separator, which isn't a part of it
|
||||||
|
|
||||||
|
|
||||||
while (token.empty() && mStream->peek() != EOF) {
|
while (token.empty() && mStream->peek() != EOF) {
|
||||||
while (mStream->peek() != EOF) { // idk why but with mStream->get(c) not all cars are loaded
|
while (mStream->peek() != EOF) { // idk why but with mStream->get(c) not all cars are loaded
|
||||||
c = static_cast<char>(mStream->get());
|
c = static_cast<char>(mStream->get());
|
||||||
|
++mPosition;
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
++mLine;
|
++mLine;
|
||||||
}
|
}
|
||||||
@@ -249,11 +251,16 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
|
|||||||
const unsigned char uc = static_cast<unsigned char>(c);
|
const unsigned char uc = static_cast<unsigned char>(c);
|
||||||
if (breakTable[uc]) {
|
if (breakTable[uc]) {
|
||||||
// separator ends token (or continues skipping if token empty)
|
// separator ends token (or continues skipping if token empty)
|
||||||
if (!token.empty())
|
if (!token.empty()) {
|
||||||
|
separated = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (token.empty()) {
|
||||||
|
mTokenBegin = mPosition - 1;
|
||||||
|
}
|
||||||
if (ToLower) c = toLowerChar(c);
|
if (ToLower) c = toLowerChar(c);
|
||||||
token.push_back(c);
|
token.push_back(c);
|
||||||
|
|
||||||
@@ -265,6 +272,8 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// NOTE: comment glued to the end of a token is counted as a part of it
|
||||||
|
mTokenEnd = separated ? mPosition - 1 : mPosition;
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -275,6 +284,7 @@ void cParser::stripFirstTokenBOM(std::string& token, bool ToLower, const char* B
|
|||||||
|
|
||||||
if (startsWithBOM(token)) {
|
if (startsWithBOM(token)) {
|
||||||
token.erase(0, 3);
|
token.erase(0, 3);
|
||||||
|
mTokenBegin += 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if first "token" was standalone BOM, read the next real token (avoid recursion)
|
// if first "token" was standalone BOM, read the next real token (avoid recursion)
|
||||||
@@ -345,7 +355,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
|
|||||||
if (sceneryLayers)
|
if (sceneryLayers)
|
||||||
{
|
{
|
||||||
// the file is still a part of the scenery, even though its content comes from the binary terrain file
|
// the file is still a part of the scenery, even though its content comes from the binary terrain file
|
||||||
scene::Layers.layer(scene::Layers.open(includefile)).binary = true;
|
scene::Layers.layer(scene::Layers.open(includefile, include_site())).binary = true;
|
||||||
scene::Layers.close();
|
scene::Layers.close();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -373,17 +383,37 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
|
|||||||
mIncludeParser->sceneryLayers = true;
|
mIncludeParser->sceneryLayers = true;
|
||||||
// content of an included scenery file forms a layer of its own. *.inc files are node templates
|
// content of an included scenery file forms a layer of its own. *.inc files are node templates
|
||||||
// reused all over the scenery, so their content stays in the layer of the file which includes them
|
// reused all over the scenery, so their content stays in the layer of the file which includes them
|
||||||
if (false == mIncludeParser->mIncFile && mIncludeParser->mSize > 0)
|
if (mIncludeParser->mIncFile)
|
||||||
{
|
{
|
||||||
scene::Layers.open(includefile);
|
scene::Layers.template_used(includefile);
|
||||||
|
}
|
||||||
|
else if (mIncludeParser->mSize > 0)
|
||||||
|
{
|
||||||
|
auto site{include_site()};
|
||||||
|
site.parameters = false == mIncludeParser->parameters.empty();
|
||||||
|
scene::Layers.open(includefile, site);
|
||||||
mIncludeParser->mLayerFile = true;
|
mIncludeParser->mLayerFile = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// describes the include directive being processed, for the scenery layer bookkeeping
|
||||||
|
scene::include_site cParser::include_site() const
|
||||||
|
{
|
||||||
|
scene::include_site site;
|
||||||
|
// the directive can be rewritten on scenery save only if we know where it is, and it sits in a layer file
|
||||||
|
site.fixed = mIncludeBegin < 0 || mFile.empty() || mIncFile;
|
||||||
|
if (false == site.fixed)
|
||||||
|
{
|
||||||
|
site.span = {mIncludeBegin, mTokenEnd};
|
||||||
|
}
|
||||||
|
return site;
|
||||||
|
}
|
||||||
|
|
||||||
bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const char* Break) {
|
bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const char* Break) {
|
||||||
// token-mode include: token == "include"
|
// token-mode include: token == "include"
|
||||||
if (expandIncludes && token == "include") {
|
if (expandIncludes && token == "include") {
|
||||||
|
mIncludeBegin = mTokenBegin;
|
||||||
std::string includefile;
|
std::string includefile;
|
||||||
if (allowRandomIncludes)
|
if (allowRandomIncludes)
|
||||||
includefile = deserialize_random_set(*this);
|
includefile = deserialize_random_set(*this);
|
||||||
@@ -399,6 +429,7 @@ bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const cha
|
|||||||
|
|
||||||
// line-mode HACK: Break == "\n\r" and line begins with "include"
|
// line-mode HACK: Break == "\n\r" and line begins with "include"
|
||||||
if (std::strcmp(Break, "\n\r") == 0 && token.compare(0, 7, "include") == 0) {
|
if (std::strcmp(Break, "\n\r") == 0 && token.compare(0, 7, "include") == 0) {
|
||||||
|
mIncludeBegin = -1; // the directive is parsed out of a line of text, its exact location isn't known
|
||||||
cParser includeparser(token.substr(7));
|
cParser includeparser(token.substr(7));
|
||||||
std::string includefile;
|
std::string includefile;
|
||||||
if (allowRandomIncludes)
|
if (allowRandomIncludes)
|
||||||
@@ -459,6 +490,7 @@ std::string cParser::readQuotes(char const Quote)
|
|||||||
bool escaped = false;
|
bool escaped = false;
|
||||||
while (mStream->get(c))
|
while (mStream->get(c))
|
||||||
{ // get all chars until the quote mark
|
{ // get all chars until the quote mark
|
||||||
|
++mPosition;
|
||||||
if (escaped)
|
if (escaped)
|
||||||
{
|
{
|
||||||
escaped = false;
|
escaped = false;
|
||||||
@@ -489,6 +521,7 @@ void cParser::skipComment(std::string const &Endmark)
|
|||||||
auto const endmarksize = Endmark.size();
|
auto const endmarksize = Endmark.size();
|
||||||
while (mStream->get(c))
|
while (mStream->get(c))
|
||||||
{
|
{
|
||||||
|
++mPosition;
|
||||||
if (c == '\n')
|
if (c == '\n')
|
||||||
{
|
{
|
||||||
// update line counter
|
// update line counter
|
||||||
@@ -626,3 +659,18 @@ int cParser::LineMain() const
|
|||||||
{
|
{
|
||||||
return mIncludeParser ? -1 : mLine;
|
return mIncludeParser ? -1 : mLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::streamoff cParser::TokenBegin() const
|
||||||
|
{
|
||||||
|
return mIncludeParser ? mIncludeParser->TokenBegin() : mTokenBegin;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamoff cParser::TokenEnd() const
|
||||||
|
{
|
||||||
|
return mIncludeParser ? mIncludeParser->TokenEnd() : mTokenEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cParser::InLayerFile() const
|
||||||
|
{
|
||||||
|
return mIncludeParser ? mIncludeParser->InLayerFile() : (false == mFile.empty() && false == mIncFile);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
|
namespace scene
|
||||||
|
{
|
||||||
|
struct include_site;
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// cParser -- generic class for parsing text data, either from file or provided string
|
// cParser -- generic class for parsing text data, either from file or provided string
|
||||||
|
|
||||||
@@ -94,6 +99,11 @@ class cParser //: public std::stringstream
|
|||||||
std::size_t Line() const;
|
std::size_t Line() const;
|
||||||
// returns number of currently processed line in main file, -1 if inside include
|
// returns number of currently processed line in main file, -1 if inside include
|
||||||
int LineMain() const;
|
int LineMain() const;
|
||||||
|
// location of the most recently read token in its source file, as byte offsets
|
||||||
|
std::streamoff TokenBegin() const;
|
||||||
|
std::streamoff TokenEnd() const;
|
||||||
|
// true if the most recently read token came straight from a scenery file which isn't an *.inc node template
|
||||||
|
bool InLayerFile() const;
|
||||||
bool expandIncludes = true;
|
bool expandIncludes = true;
|
||||||
bool allowRandomIncludes = false;
|
bool allowRandomIncludes = false;
|
||||||
bool skipComments = true;
|
bool skipComments = true;
|
||||||
@@ -101,6 +111,7 @@ class cParser //: public std::stringstream
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void startIncludeFromParser(cParser &srcParser, bool ToLower, std::string includefile);
|
void startIncludeFromParser(cParser &srcParser, bool ToLower, std::string includefile);
|
||||||
|
scene::include_site include_site() const;
|
||||||
bool handleIncludeIfPresent(std::string &token, bool ToLower, const char *Break);
|
bool handleIncludeIfPresent(std::string &token, bool ToLower, const char *Break);
|
||||||
// methods:
|
// methods:
|
||||||
void readToken(std::string& out, bool ToLower = true, const char *Break = "\n\r\t ;");
|
void readToken(std::string& out, bool ToLower = true, const char *Break = "\n\r\t ;");
|
||||||
@@ -120,6 +131,10 @@ class cParser //: public std::stringstream
|
|||||||
std::size_t mLine { 0 }; // currently processed line
|
std::size_t mLine { 0 }; // currently processed line
|
||||||
bool mIncFile { false }; // the parser is processing an *.inc file
|
bool mIncFile { false }; // the parser is processing an *.inc file
|
||||||
bool mLayerFile { false }; // the parser is processing a file registered as scenery layer
|
bool mLayerFile { false }; // the parser is processing a file registered as scenery layer
|
||||||
|
std::streamoff mPosition { 0 }; // amount of bytes read from the stream so far
|
||||||
|
std::streamoff mTokenBegin { 0 }; // location of the most recently read token
|
||||||
|
std::streamoff mTokenEnd { 0 };
|
||||||
|
std::streamoff mIncludeBegin { -1 }; // location of the include directive being processed, -1 if unknown
|
||||||
bool mFirstToken { true }; // processing first token in the current file; helper used when checking for utf bom
|
bool mFirstToken { true }; // processing first token in the current file; helper used when checking for utf bom
|
||||||
typedef std::map<std::string, std::string> commentmap;
|
typedef std::map<std::string, std::string> commentmap;
|
||||||
commentmap mComments {
|
commentmap mComments {
|
||||||
|
|||||||
Reference in New Issue
Block a user