mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-28 07:37:28 +01:00
feat(editor): edit parameters of a placed include
This commit is contained in:
@@ -675,10 +675,8 @@ void editor_mode::undo_last()
|
||||
|
||||
if (snap.instance != 0)
|
||||
{
|
||||
// include of a scenery template was placed or removed; either is taken back by flipping its removal
|
||||
toggle_include(snap.instance);
|
||||
restore_include(snap);
|
||||
g_redo.push_back(std::move(snap));
|
||||
m_instance = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -766,9 +764,8 @@ void editor_mode::redo_last()
|
||||
|
||||
if (snap.instance != 0)
|
||||
{
|
||||
toggle_include(snap.instance);
|
||||
restore_include(snap);
|
||||
m_history.push_back(std::move(snap));
|
||||
m_instance = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -926,8 +923,27 @@ bool editor_mode::update()
|
||||
}
|
||||
if (m_instance != 0 && false == scene::Layers.removable(m_instance))
|
||||
{
|
||||
m_instance = 0;
|
||||
select_include(0);
|
||||
}
|
||||
if (m_include.changed)
|
||||
{
|
||||
// a parameter of the selected include was changed in the node properties
|
||||
m_include.changed = false;
|
||||
apply_include();
|
||||
}
|
||||
if (false == m_include.active && false == m_gizmo_using)
|
||||
{
|
||||
m_include_gesture = false;
|
||||
}
|
||||
// models replaced after changes of includes are destroyed once the renderer can't have them among its pick candidates
|
||||
for (auto &retired : m_retired)
|
||||
{
|
||||
if (--retired.second <= 0)
|
||||
{
|
||||
simulation::Instances.purge(retired.first);
|
||||
}
|
||||
}
|
||||
m_retired.erase(std::remove_if(m_retired.begin(), m_retired.end(), [](std::pair<TAnimModel *, int> const &Retired) { return Retired.second <= 0; }), m_retired.end());
|
||||
|
||||
simulation::Region->update_sounds();
|
||||
audio::renderer.update(Global.iPause ? 0.0 : deltarealtime);
|
||||
@@ -1615,6 +1631,97 @@ void editor_mode::toggle_include(scene::instance_handle const Instance)
|
||||
ui()->set_status("Include of \"" + *included.file + "\" " + (included.removed ? "removed." : "restored."));
|
||||
}
|
||||
|
||||
void editor_mode::restore_include(EditorSnapshot &Snapshot)
|
||||
{
|
||||
if (Snapshot.serialized.empty())
|
||||
{
|
||||
// the include was placed or removed; either is taken back by flipping its removal
|
||||
toggle_include(Snapshot.instance);
|
||||
select_include(0);
|
||||
return;
|
||||
}
|
||||
// parameters of the include were changed. the snapshot holds the directive to go back to, and leaves with the current one
|
||||
auto current = scene::Layers.directive(Snapshot.instance);
|
||||
if (current.empty() || scene::Layers.instance(Snapshot.instance).dead)
|
||||
{
|
||||
ui()->set_status("Include of \"" + Snapshot.node_name + "\" is gone from the scenery, the change can't be taken back.", true);
|
||||
return;
|
||||
}
|
||||
set_include_directive(Snapshot.instance, Snapshot.serialized);
|
||||
Snapshot.serialized = std::move(current);
|
||||
select_include(scene::Layers.removable(Snapshot.instance) ? Snapshot.instance : 0);
|
||||
}
|
||||
|
||||
void editor_mode::select_include(scene::instance_handle const Instance)
|
||||
{
|
||||
m_instance = (scene::Layers.tracked(Instance) ? Instance : 0);
|
||||
m_include = include_selection{};
|
||||
m_include_gesture = false;
|
||||
if (m_instance == 0)
|
||||
{
|
||||
ui()->set_include(nullptr);
|
||||
return;
|
||||
}
|
||||
auto const &included = scene::Layers.instance(m_instance);
|
||||
m_include.instance = m_instance;
|
||||
m_include.target = *included.file;
|
||||
// NOTE: a template without a usable description is still presented, with its parameters unnamed
|
||||
std::string error;
|
||||
editor_includes::load(*included.file, m_include.info, error);
|
||||
if (false == editor_includes::parse_directive(scene::Layers.directive(m_instance), m_include.target, m_include.values))
|
||||
{
|
||||
m_include.values.clear();
|
||||
m_include.issue = "The directive of the include can't be read from the scenery file, its parameters can't be changed.";
|
||||
}
|
||||
// with a rotation or a scale in effect the parameters don't translate to a location in the scene in a single way,
|
||||
// see place_include()
|
||||
m_include.placement = (included.context.rotation == glm::vec3{0.f} && included.context.scale == glm::vec3{1.f});
|
||||
ui()->set_include(&m_include);
|
||||
}
|
||||
|
||||
void editor_mode::set_include_directive(scene::instance_handle const Instance, std::string const &Directive)
|
||||
{
|
||||
if (false == scene::Layers.modify(Instance, Directive))
|
||||
return;
|
||||
std::vector<TAnimModel *> retired;
|
||||
simulation::State.rebuild_include(Instance, retired);
|
||||
// the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these
|
||||
for (auto *model : retired)
|
||||
m_retired.emplace_back(model, 120);
|
||||
}
|
||||
|
||||
void editor_mode::apply_include()
|
||||
{
|
||||
if (m_instance == 0 || false == m_include.issue.empty())
|
||||
return;
|
||||
for (auto const &value : m_include.values)
|
||||
{
|
||||
if (value == "end" || value.find_first_of("\"\r\n") != std::string::npos)
|
||||
{
|
||||
// either would cut the directive short
|
||||
ui()->set_status("A parameter of an include can't be \"end\", or have a quotation mark in it.", true);
|
||||
select_include(m_instance); // back to the values of the directive
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto before = scene::Layers.directive(m_instance);
|
||||
auto const directive = editor_includes::compose_directive(m_include.target, m_include.values);
|
||||
if (directive == before)
|
||||
return;
|
||||
if (false == m_include_gesture)
|
||||
{
|
||||
// a single undo step for the whole drag
|
||||
EditorSnapshot snap;
|
||||
snap.instance = m_instance;
|
||||
snap.node_name = *scene::Layers.instance(m_instance).file;
|
||||
snap.serialized = std::move(before);
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
m_include_gesture = true;
|
||||
}
|
||||
set_include_directive(m_instance, directive);
|
||||
}
|
||||
|
||||
void editor_mode::place_include(std::string const &File, int const RotationMode, float const FixedRotation)
|
||||
{
|
||||
if (scene::Layers.empty())
|
||||
@@ -1670,6 +1777,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode,
|
||||
snap.node_name = File;
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
select_include(instance);
|
||||
|
||||
ui()->set_status(
|
||||
"\"" + info.name + "\" placed in layer \"" + scene::Layers.layer(layer).name + "\": " + std::to_string(preview.first) + " model(s) shown"
|
||||
@@ -2097,7 +2205,15 @@ void editor_mode::render_gizmo_options()
|
||||
if (m_gizmo_snap < 0.0f)
|
||||
m_gizmo_snap = 0.0f;
|
||||
}
|
||||
if (!m_node)
|
||||
if (m_instance != 0)
|
||||
{
|
||||
auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 ||
|
||||
editor_includes::parameter_with_role(m_include.info, "pos.z") != 0;
|
||||
ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" :
|
||||
false == m_include.placement ? "Include selected, no gizmo under rotate or scale" :
|
||||
"Include selected, axes as its template allows");
|
||||
}
|
||||
else if (!m_node)
|
||||
ImGui::TextDisabled("No node selected");
|
||||
}
|
||||
|
||||
@@ -2110,7 +2226,7 @@ void editor_mode::render_gizmo()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_node)
|
||||
if (!m_node && m_instance == 0)
|
||||
{
|
||||
m_gizmo_using = false;
|
||||
return;
|
||||
@@ -2134,6 +2250,12 @@ void editor_mode::render_gizmo()
|
||||
float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f;
|
||||
glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f);
|
||||
|
||||
if (!m_node)
|
||||
{
|
||||
render_include_gizmo(view, projection, camerapos);
|
||||
return;
|
||||
}
|
||||
|
||||
// rotation/scale are only meaningful for instanced models; other node types translate only
|
||||
TAnimModel *model = dynamic_cast<TAnimModel *>(m_node);
|
||||
|
||||
@@ -2211,6 +2333,103 @@ void editor_mode::render_gizmo()
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos)
|
||||
{
|
||||
m_gizmo_using = false;
|
||||
if (false == m_include.placement || false == m_include.issue.empty())
|
||||
return;
|
||||
|
||||
// parameters of the template the gizmo can drive, as indices of their values; -1 where the template has none
|
||||
static char const *const positionroles[] = {"pos.x", "pos.y", "pos.z"};
|
||||
static char const *const rotationroles[] = {"rot.x", "rot.y", "rot.z"};
|
||||
auto const numeric_parameter = [this](char const *Role, double &Value) {
|
||||
auto const index = editor_includes::parameter_with_role(m_include.info, Role) - 1;
|
||||
if (index < 0 || index >= static_cast<int>(m_include.values.size()))
|
||||
return -1;
|
||||
auto const &text = m_include.values[index];
|
||||
char *end = nullptr;
|
||||
auto const number = std::strtod(text.c_str(), &end);
|
||||
if (text.empty() || *end != '\0')
|
||||
return -1;
|
||||
Value = number;
|
||||
return index;
|
||||
};
|
||||
int position[3], rotation[3];
|
||||
double location[3] = {0.0, 0.0, 0.0}, angles[3] = {0.0, 0.0, 0.0};
|
||||
auto positions = 0, rotations = 0;
|
||||
for (auto axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
position[axis] = numeric_parameter(positionroles[axis], location[axis]);
|
||||
rotation[axis] = numeric_parameter(rotationroles[axis], angles[axis]);
|
||||
positions += (position[axis] >= 0 ? 1 : 0);
|
||||
rotations += (rotation[axis] >= 0 ? 1 : 0);
|
||||
}
|
||||
if (positions == 0)
|
||||
return; // nothing tells where the include is
|
||||
|
||||
// only the axes the template has parameters for can be manipulated
|
||||
auto operation = 0;
|
||||
for (auto axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
if (m_gizmo_op == gizmo_operation::translate && position[axis] >= 0)
|
||||
operation |= (ImGuizmo::TRANSLATE_X << axis);
|
||||
else if (m_gizmo_op == gizmo_operation::rotate && rotation[axis] >= 0)
|
||||
operation |= (ImGuizmo::ROTATE_X << axis);
|
||||
}
|
||||
if (operation == 0)
|
||||
return;
|
||||
auto const translating = (m_gizmo_op == gizmo_operation::translate);
|
||||
|
||||
// the parameters are relative to the origin in effect at the directive
|
||||
auto const &offset = scene::Layers.instance(m_instance).context.offset;
|
||||
glm::vec3 const relativepos = glm::vec3(offset + glm::dvec3{location[0], location[1], location[2]} - Camerapos);
|
||||
float const translation[3] = {relativepos.x, relativepos.y, relativepos.z};
|
||||
float const rotationangles[3] = {static_cast<float>(angles[0]), static_cast<float>(angles[1]), static_cast<float>(angles[2])};
|
||||
float const scale[3] = {1.0f, 1.0f, 1.0f};
|
||||
glm::mat4 matrix(1.0f);
|
||||
ImGuizmo::RecomposeMatrixFromComponents(translation, rotationangles, scale, glm::value_ptr(matrix));
|
||||
|
||||
// moving along the axes of the include would need all three of them
|
||||
ImGuizmo::MODE const mode = (m_gizmo_local && (false == translating || positions == 3)) ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
||||
glm::vec3 const snapvalue(translating ? m_gizmo_snap : 5.0f);
|
||||
float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr;
|
||||
|
||||
ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast<ImGuizmo::OPERATION>(operation), mode, glm::value_ptr(matrix), nullptr, snap);
|
||||
|
||||
if (false == ImGuizmo::IsUsing())
|
||||
return;
|
||||
m_gizmo_using = true;
|
||||
|
||||
float newtranslation[3], newrotation[3], newscale[3];
|
||||
ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(matrix), newtranslation, newrotation, newscale);
|
||||
if (translating)
|
||||
{
|
||||
glm::dvec3 const newlocation = Camerapos + glm::dvec3{newtranslation[0], newtranslation[1], newtranslation[2]} - offset;
|
||||
for (auto axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
if (position[axis] >= 0)
|
||||
m_include.values[position[axis]] = editor_includes::number(newlocation[axis]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rotations == 1)
|
||||
{
|
||||
// the decomposition spreads a turn of more than a quarter over all three angles;
|
||||
// with a single axis to turn around the angle is taken straight from the matrix
|
||||
newrotation[0] = glm::degrees(std::atan2(matrix[1][2], matrix[1][1]));
|
||||
newrotation[1] = glm::degrees(std::atan2(-matrix[0][2], matrix[0][0]));
|
||||
newrotation[2] = glm::degrees(std::atan2(matrix[0][1], matrix[0][0]));
|
||||
}
|
||||
for (auto axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
if (rotation[axis] >= 0)
|
||||
m_include.values[rotation[axis]] = editor_includes::number(clamp_circular(newrotation[axis]));
|
||||
}
|
||||
}
|
||||
apply_include();
|
||||
}
|
||||
|
||||
void editor_mode::update_camera(double const Deltatime)
|
||||
{
|
||||
Camera.Update();
|
||||
@@ -2294,7 +2513,7 @@ void editor_mode::exit()
|
||||
|
||||
// drop selection so a stale/dangling node pointer isn't used on the next editor session
|
||||
m_node = nullptr;
|
||||
m_instance = 0;
|
||||
select_include(0);
|
||||
m_gizmo_using = false;
|
||||
ui()->set_node(nullptr);
|
||||
|
||||
@@ -2400,7 +2619,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved.");
|
||||
m_instance = 0;
|
||||
select_include(0);
|
||||
break;
|
||||
}
|
||||
if (is_press(Action))
|
||||
@@ -2526,7 +2745,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
return;
|
||||
|
||||
m_node = nullptr;
|
||||
m_instance = 0;
|
||||
select_include(0);
|
||||
|
||||
// in a scenery opened for editing selection has its limits. nodes of locked layers are off limits.
|
||||
// nodes defined by a scenery template can't be rewritten one by one on save, so a click on one
|
||||
@@ -2536,8 +2755,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
std::string reason;
|
||||
if (node->from_template() && scene::Layers.removable(node->m_instance, &reason))
|
||||
{
|
||||
m_instance = node->m_instance;
|
||||
ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Del removes it from the scenery.");
|
||||
select_include(node->m_instance);
|
||||
ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Its parameters are in the node properties, Del removes it from the scenery.");
|
||||
node = nullptr;
|
||||
}
|
||||
else if (node->from_template() || false == scene::Layers.editable(node, &reason))
|
||||
|
||||
@@ -129,6 +129,9 @@ class editor_mode : public application_mode
|
||||
scene::basic_editor m_editor;
|
||||
scene::basic_node *m_node{nullptr}; // currently selected scene node
|
||||
scene::instance_handle m_instance{0}; // currently selected include of a scenery template; these are handled as a whole
|
||||
include_selection m_include; // parameters of the selected include, changed with the gizmo and in the node properties
|
||||
bool m_include_gesture{false}; // a change of the selected include is under way, and has its undo snapshot already
|
||||
std::vector<std::pair<TAnimModel *, int>> m_retired; // models replaced after changes of includes, with the frames left until they're destroyed
|
||||
bool m_takesnapshot{true}; // helper, hints whether snapshot of selected node(s) should be taken before modification
|
||||
bool m_dragging = false;
|
||||
glm::dvec3 oldPos; // world position of the last brush placement
|
||||
@@ -173,6 +176,16 @@ class editor_mode : public application_mode
|
||||
void place_include(std::string const &File, int RotationMode, float FixedRotation);
|
||||
// undo and redo of placing or removing an include: brings removed include back, or removes it
|
||||
void toggle_include(scene::instance_handle Instance);
|
||||
// makes specified include the selected one, 0 clears the selection
|
||||
void select_include(scene::instance_handle Instance);
|
||||
// writes parameter values of the selected include to its directive, and shows the outcome in the scene
|
||||
void apply_include();
|
||||
// replaces directive of specified include, and the models shown for it
|
||||
void set_include_directive(scene::instance_handle Instance, std::string const &Directive);
|
||||
// undo and redo of an include snapshot
|
||||
void restore_include(EditorSnapshot &Snapshot);
|
||||
// gizmo for the selected include, limited to what the parameters of its template can express
|
||||
void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos);
|
||||
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
|
||||
void export_scenery();
|
||||
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode
|
||||
|
||||
@@ -164,6 +164,11 @@ void editor_ui::set_node(scene::basic_node *Node)
|
||||
m_node = Node;
|
||||
}
|
||||
|
||||
void editor_ui::set_include(include_selection *Include)
|
||||
{
|
||||
m_itempropertiespanel.include(Include);
|
||||
}
|
||||
|
||||
void editor_ui::add_node_template(const std::string &desc)
|
||||
{
|
||||
m_nodebankpanel.add_template(desc);
|
||||
|
||||
@@ -29,6 +29,8 @@ class editor_ui : public ui_layer
|
||||
// updates state of UI elements
|
||||
void update() override;
|
||||
void set_node(scene::basic_node *Node);
|
||||
// include of a scenery template to show in the node properties in place of a node; nullptr if there's none
|
||||
void set_include(include_selection *Include);
|
||||
void add_node_template(const std::string &desc);
|
||||
float rot_val();
|
||||
bool rot_from_last();
|
||||
|
||||
@@ -312,6 +312,11 @@ void itemproperties_panel::render()
|
||||
|
||||
void itemproperties_panel::render_body()
|
||||
{
|
||||
if (m_include != nullptr)
|
||||
{
|
||||
render_include();
|
||||
return;
|
||||
}
|
||||
// header section
|
||||
for (auto const &line : text_lines)
|
||||
{
|
||||
@@ -1187,6 +1192,74 @@ ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f};
|
||||
|
||||
} // namespace
|
||||
|
||||
void itemproperties_panel::render_include()
|
||||
{
|
||||
auto &include{*m_include};
|
||||
include.active = false;
|
||||
|
||||
ImGui::Text("include: %s", Bezogonkow(include.target).c_str());
|
||||
if (false == include.info.name.empty())
|
||||
{
|
||||
ImGui::TextDisabled("%s", include.info.name.c_str());
|
||||
}
|
||||
if (false == include.issue.empty())
|
||||
{
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(include.issue).c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
return;
|
||||
}
|
||||
if (include.values.empty())
|
||||
{
|
||||
ImGui::TextDisabled("The template takes no parameters.");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushItemWidth(ImGui::GetFontSize() * 14.0f);
|
||||
for (std::size_t idx = 0; idx < include.values.size(); ++idx)
|
||||
{
|
||||
auto const id{static_cast<int>(idx + 1)};
|
||||
auto const lookup{std::find_if(std::begin(include.info.parameters), std::end(include.info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
|
||||
auto const described{lookup != std::end(include.info.parameters)};
|
||||
auto const role{described ? lookup->role : std::string{"free"}};
|
||||
// the label from the description, or what the description tells about the parameter
|
||||
auto const label{described && false == lookup->label.empty() ? lookup->label : role != "free" ? role : "(p" + std::to_string(id) + ")"};
|
||||
auto &value{include.values[idx]};
|
||||
|
||||
ImGui::PushID(id);
|
||||
char *end{nullptr};
|
||||
auto number{std::strtod(value.c_str(), &end)};
|
||||
if ((role.starts_with("pos.") || role.starts_with("rot.") || role == "number") && false == value.empty() && *end == '\0')
|
||||
{
|
||||
if (ImGui::DragScalar(label.c_str(), ImGuiDataType_Double, &number, role.starts_with("pos.") ? 0.05f : 0.5f, nullptr, nullptr, "%.3f"))
|
||||
{
|
||||
value = editor_includes::number(number);
|
||||
include.changed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// applied once the field is left, so the template isn't processed with a half-typed value
|
||||
input_text(label.c_str(), value);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
include.changed = true;
|
||||
}
|
||||
}
|
||||
include.active |= ImGui::IsItemActive();
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("(p%d), %s", id, role.c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
||||
ImGui::TextWrapped("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again.");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void layers_panel::report(std::string const &Status, bool const Error)
|
||||
{
|
||||
status = Status;
|
||||
|
||||
@@ -29,6 +29,20 @@ struct item_properties {
|
||||
changeable<glm::vec3> rotation {};
|
||||
};
|
||||
*/
|
||||
// include of a scenery template selected in the editor, with the values its directive passes to the template.
|
||||
// owned by the editor mode; the node properties let the user change the values
|
||||
struct include_selection
|
||||
{
|
||||
scene::instance_handle instance{0};
|
||||
std::string target; // name of the template, the way the directive spells it
|
||||
include_info info; // description of the template
|
||||
std::vector<std::string> values; // value of each parameter in turn
|
||||
bool placement{false}; // the position and rotation parameters can be driven by the gizmo
|
||||
std::string issue; // why the values can't be changed, if they can't
|
||||
bool changed{false}; // set by the UI when a value was changed, cleared by the editor mode once the change is applied
|
||||
bool active{false}; // the UI is in the middle of changing a value
|
||||
};
|
||||
|
||||
class itemproperties_panel : public ui_panel
|
||||
{
|
||||
|
||||
@@ -39,6 +53,11 @@ class itemproperties_panel : public ui_panel
|
||||
// position/rotation/scale of TAnimModel nodes in place. Other node types
|
||||
// are still treated as read-only.
|
||||
void update(scene::basic_node *Node);
|
||||
// binds include of a scenery template to the panel, shown in place of a node; nullptr unbinds
|
||||
void include(include_selection *Include)
|
||||
{
|
||||
m_include = Include;
|
||||
}
|
||||
void render() override;
|
||||
// panel content without the window, also drawn inside the toolset window
|
||||
void render_body();
|
||||
@@ -50,9 +69,12 @@ class itemproperties_panel : public ui_panel
|
||||
// renders DragFloat3/DragScalarN widgets for position, rotation and scale
|
||||
// of the currently bound TAnimModel; no-op for other node subclasses.
|
||||
void render_transform_editor();
|
||||
// renders widgets for the parameters of the bound include
|
||||
void render_include();
|
||||
|
||||
// members
|
||||
scene::basic_node *m_node{nullptr}; // scene node bound to the panel
|
||||
include_selection *m_include{nullptr}; // include of a scenery template bound to the panel
|
||||
scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel
|
||||
std::string m_groupprefix;
|
||||
std::vector<text_line> m_grouplines;
|
||||
|
||||
@@ -543,15 +543,99 @@ bool complete(include_info const &Info, int const Parameters, std::string *Issue
|
||||
return issue.empty();
|
||||
}
|
||||
|
||||
std::string number(double const Value)
|
||||
{
|
||||
std::ostringstream converter;
|
||||
converter.imbue(std::locale::classic());
|
||||
converter << std::fixed << std::setprecision(3) << Value;
|
||||
return converter.str();
|
||||
}
|
||||
|
||||
int parameter_with_role(include_info const &Info, std::string const &Role)
|
||||
{
|
||||
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [&](include_parameter const &Parameter) { return Parameter.role == Role; })};
|
||||
return lookup != std::end(Info.parameters) ? lookup->id : 0;
|
||||
}
|
||||
|
||||
bool parse_directive(std::string const &Directive, std::string &File, std::vector<std::string> &Values)
|
||||
{
|
||||
// the same rules the scenery parser goes by, except the text is taken as it is written
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
auto const flush = [&]() {
|
||||
if (false == token.empty())
|
||||
{
|
||||
tokens.emplace_back(token);
|
||||
token.clear();
|
||||
}
|
||||
};
|
||||
for (std::size_t idx = 0; idx < Directive.size(); ++idx)
|
||||
{
|
||||
auto const character{Directive[idx]};
|
||||
if (character == '\"')
|
||||
{
|
||||
flush();
|
||||
auto const end{Directive.find('\"', idx + 1)};
|
||||
if (end == std::string::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
tokens.emplace_back(Directive, idx + 1, end - idx - 1);
|
||||
idx = end;
|
||||
}
|
||||
else if (Directive.compare(idx, 2, "//") == 0)
|
||||
{
|
||||
flush();
|
||||
idx = Directive.find('\n', idx);
|
||||
if (idx == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (Directive.compare(idx, 2, "/*") == 0)
|
||||
{
|
||||
flush();
|
||||
idx = Directive.find("*/", idx + 2);
|
||||
if (idx == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
else if (character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == ';')
|
||||
{
|
||||
flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
token += character;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
if (tokens.size() < 3 || ToLower(tokens.front()) != "include" || tokens.back() != "end")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
File = tokens[1];
|
||||
Values.assign(std::begin(tokens) + 2, std::end(tokens) - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string compose_directive(std::string const &File, std::vector<std::string> const &Values)
|
||||
{
|
||||
auto const quoted = [](std::string const &Text) { return Text.find_first_of(" \t;") != std::string::npos ? '\"' + Text + '\"' : Text; };
|
||||
std::string text{"include " + quoted(File)};
|
||||
for (auto const &value : Values)
|
||||
{
|
||||
// a parameter can't be left out, as the ones after it would take its place
|
||||
text += ' ' + (value.empty() ? std::string{"none"} : quoted(value));
|
||||
}
|
||||
return text + " end";
|
||||
}
|
||||
|
||||
std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional<float> Yaw)
|
||||
{
|
||||
auto const number = [](double const Value) {
|
||||
std::ostringstream converter;
|
||||
converter.imbue(std::locale::classic());
|
||||
converter << std::fixed << std::setprecision(3) << Value;
|
||||
return converter.str();
|
||||
};
|
||||
std::string text{"include " + File};
|
||||
std::vector<std::string> values;
|
||||
for (auto id = 1; id <= Parameters; ++id)
|
||||
{
|
||||
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
|
||||
@@ -585,14 +669,9 @@ std::string directive(std::string const &File, include_info const &Info, int con
|
||||
std::snprintf(suffix, sizeof(suffix), "_%08x", static_cast<unsigned int>(engine()));
|
||||
value = (value.empty() ? std::filesystem::path(File).stem().string() : value) + suffix;
|
||||
}
|
||||
if (value.empty())
|
||||
{
|
||||
// a parameter can't be left out, as the ones after it would take its place
|
||||
value = "none";
|
||||
}
|
||||
text += ' ' + (value.find_first_of(" \t;") != std::string::npos ? '\"' + value + '\"' : value);
|
||||
values.emplace_back(value);
|
||||
}
|
||||
return text + " end";
|
||||
return compose_directive(File, values);
|
||||
}
|
||||
|
||||
} // namespace editor_includes
|
||||
|
||||
@@ -68,6 +68,16 @@ bool complete(include_info const &Info, int Parameters, std::string *Issue = nul
|
||||
// rotation parameters receive their default values
|
||||
std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional<float> Yaw);
|
||||
|
||||
// splits include directive into the name of the included file and the values of the parameters, both as they're
|
||||
// written. returns: false if the text isn't a complete include directive
|
||||
bool parse_directive(std::string const &Directive, std::string &File, std::vector<std::string> &Values);
|
||||
// builds include directive out of the name of the included file and the values of the parameters
|
||||
std::string compose_directive(std::string const &File, std::vector<std::string> const &Values);
|
||||
// text of a number passed as a parameter value
|
||||
std::string number(double Value);
|
||||
// number of the parameter with specified role, 0 if the description has none
|
||||
int parameter_with_role(include_info const &Info, std::string const &Role);
|
||||
|
||||
// marks node bank template which stands for a scenery template rather than for definition of a single node
|
||||
extern std::string const directive_mark;
|
||||
|
||||
|
||||
@@ -210,8 +210,8 @@ void node_layers::visible(layer_handle Layer, bool const Visible)
|
||||
}
|
||||
if (Visible)
|
||||
{
|
||||
// nodes of a removed include stay out of sight
|
||||
if (Node->m_layerhidden && (false == tracked(Node->m_instance) || false == instance(Node->m_instance).removed))
|
||||
// nodes of a removed include stay out of sight, and so do the ones the editor replaced with its own
|
||||
if (Node->m_layerhidden && (false == tracked(Node->m_instance) || (false == instance(Node->m_instance).removed && false == stale(Node))))
|
||||
{
|
||||
Node->visible(true);
|
||||
Node->m_layerhidden = false;
|
||||
@@ -613,7 +613,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin
|
||||
{
|
||||
// directives of includes placed in the editor are prepared for the end of their layer file
|
||||
auto const unsaved{std::any_of(std::begin(m_instances), std::end(m_instances), [&](include_instance const &Instance) {
|
||||
return false == Instance.dead && false == Instance.removed && false == Instance.directive.empty() && resolve(Instance.layer) == Source;
|
||||
return false == Instance.dead && false == Instance.removed && false == Instance.span.valid() && resolve(Instance.layer) == Source;
|
||||
})};
|
||||
if (unsaved)
|
||||
{
|
||||
@@ -695,6 +695,7 @@ void node_layers::instance_begin(std::string const &File, source_span const &Spa
|
||||
included.layer = handle();
|
||||
included.file = &(*m_templates.emplace(File).first);
|
||||
included.span = Span;
|
||||
included.context = m_context;
|
||||
m_instance = static_cast<instance_handle>(m_instances.size());
|
||||
}
|
||||
|
||||
@@ -741,7 +742,7 @@ void node_layers::removed(instance_handle Instance, bool Removed)
|
||||
auto const includelayer{resolve(m_instances[Instance - 1].layer)};
|
||||
auto const layervisible{false == valid(includelayer) || layer(includelayer).visible};
|
||||
auto const update_node = [=, this](basic_node *Node, layer_item const Item) {
|
||||
if (Node == nullptr || Node->m_instance != Instance)
|
||||
if (Node == nullptr || Node->m_instance != Instance || (Item == layer_item::model && stale(Node)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -791,6 +792,67 @@ instance_handle node_layers::place(layer_handle Layer, std::string const &File,
|
||||
return static_cast<instance_handle>(m_instances.size());
|
||||
}
|
||||
|
||||
// text of the directive of specified include, as changed in the editor or as it stands in the scenery file
|
||||
std::string node_layers::directive(instance_handle const Instance) const
|
||||
{
|
||||
if (false == tracked(Instance))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
auto const &included{instance(Instance)};
|
||||
if (false == included.directive.empty() || false == included.span.valid() || false == valid(included.layer))
|
||||
{
|
||||
return included.directive;
|
||||
}
|
||||
std::string text(static_cast<std::size_t>(included.span.end - included.span.begin), '\0');
|
||||
std::ifstream input{path(included.layer), std::ios_base::binary};
|
||||
if (false == input.is_open() || false == input.seekg(included.span.begin).good() || false == input.read(text.data(), static_cast<std::streamsize>(text.size())).good())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// replaces the directive of specified include, the scenery file receives it on save
|
||||
bool node_layers::modify(instance_handle const Instance, std::string const &Directive)
|
||||
{
|
||||
if (false == tracked(Instance) || m_instances[Instance - 1].dead || Directive.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_instances[Instance - 1].directive = Directive;
|
||||
return true;
|
||||
}
|
||||
|
||||
// takes the models specified include was loaded with out of the scene, once the editor shows its own in their place
|
||||
void node_layers::rebuilt(instance_handle const Instance)
|
||||
{
|
||||
if (false == tracked(Instance) || m_instances[Instance - 1].rebuilt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto const removed{m_instances[Instance - 1].removed};
|
||||
for (auto *model : simulation::Instances.sequence())
|
||||
{
|
||||
if (model == nullptr || model->m_instance != Instance || model->m_preview)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// NOTE: the models themselves are left alone, events of the scenery can refer to them
|
||||
simulation::Region->erase(model);
|
||||
if (false == removed)
|
||||
{
|
||||
count(model->layer(), layer_item::model, -1);
|
||||
}
|
||||
if (model->visible())
|
||||
{
|
||||
model->visible(false);
|
||||
model->m_layerhidden = true;
|
||||
}
|
||||
}
|
||||
m_instances[Instance - 1].rebuilt = true;
|
||||
}
|
||||
|
||||
} // namespace scene
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
@@ -100,10 +100,11 @@ struct include_instance
|
||||
layer_handle layer{null_handle}; // layer of the file holding the directive
|
||||
std::string const *file{nullptr}; // name of the included template
|
||||
source_span span; // location of the whole directive in the file; invalid for directive which wasn't saved yet
|
||||
std::string directive; // text of the directive, for include placed in the editor and not saved yet
|
||||
layer_context context; // placement the directive of include placed in the editor was prepared for
|
||||
std::string directive; // text of the directive, for include placed or changed in the editor and not saved yet
|
||||
layer_context context; // placement in effect at the directive
|
||||
bool removed{false}; // the include is to be dropped from the scenery
|
||||
bool dead{false}; // removal of the include was saved
|
||||
bool rebuilt{false}; // models the include was loaded with gave way to the ones made by the editor
|
||||
};
|
||||
|
||||
// marks nodes defined by a template which was included in a way the editor doesn't keep track of
|
||||
@@ -277,6 +278,18 @@ class node_layers
|
||||
// registers include directive made in the editor, to be written to the file of specified layer on save.
|
||||
// the directive has to be prepared for the placement in effect at the end of that file. returns: handle to the include
|
||||
instance_handle place(layer_handle Layer, std::string const &File, std::string const &Directive);
|
||||
// text of the directive of specified include, as changed in the editor or as it stands in the scenery file.
|
||||
// returns: empty text if the directive can't be read
|
||||
std::string directive(instance_handle Instance) const;
|
||||
// replaces the directive of specified include, the scenery file receives it on save. NOTE: the nodes aren't touched
|
||||
bool modify(instance_handle Instance, std::string const &Directive);
|
||||
// takes the models specified include was loaded with out of the scene, once the editor shows its own in their place
|
||||
void rebuilt(instance_handle Instance);
|
||||
// true if specified node was replaced by the editor with a node showing the current state of its include
|
||||
bool stale(basic_node const *Node) const
|
||||
{
|
||||
return tracked(Node->m_instance) && instance(Node->m_instance).rebuilt && false == Node->m_preview;
|
||||
}
|
||||
// true if specified layer is included, directly or not, by the other one
|
||||
bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const;
|
||||
|
||||
|
||||
@@ -275,6 +275,7 @@ struct node_layers::file_patch
|
||||
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
|
||||
instance_handle instance{0}; // include of a template made by the directive in the replacement
|
||||
layer_handle inlined{null_handle}; // layer whose content replaces the range
|
||||
};
|
||||
std::vector<edit> edits;
|
||||
@@ -460,6 +461,10 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi
|
||||
{
|
||||
Output.sites.push_back({edit.site, span});
|
||||
}
|
||||
if (edit.instance != 0)
|
||||
{
|
||||
Output.instances.push_back({edit.instance, span});
|
||||
}
|
||||
Output.text.append(edit.text, edit.lead - lead, std::string::npos);
|
||||
}
|
||||
cursor = end;
|
||||
@@ -692,6 +697,17 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
edit.end = included.span.end;
|
||||
state.patches[included.layer].edits.emplace_back(std::move(edit));
|
||||
}
|
||||
else if (false == included.directive.empty())
|
||||
{
|
||||
// changed in the editor, the new directive takes the place of the one in the file
|
||||
file_patch::edit edit;
|
||||
edit.begin = included.span.begin;
|
||||
edit.end = included.span.end;
|
||||
edit.length = included.directive.size();
|
||||
edit.text = included.directive;
|
||||
edit.instance = static_cast<instance_handle>(idx + 1);
|
||||
state.patches[included.layer].edits.emplace_back(std::move(edit));
|
||||
}
|
||||
}
|
||||
else if (false == included.removed && false == included.directive.empty())
|
||||
{
|
||||
@@ -840,7 +856,7 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
for (std::size_t idx = 0; idx < m_instances.size(); ++idx)
|
||||
{
|
||||
auto const &included{m_instances[idx]};
|
||||
if (false == included.dead && false == included.removed && included.span.valid() && state.content.count(included.layer) != 0)
|
||||
if (false == included.dead && false == included.removed && included.directive.empty() && included.span.valid() && state.content.count(included.layer) != 0)
|
||||
{
|
||||
state.instances[included.layer].emplace_back(static_cast<instance_handle>(idx + 1));
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ public:
|
||||
std::string m_name;
|
||||
bool m_dirty { false };
|
||||
bool m_layerhidden { false }; // the node was visible until the editor hid its scenery layer, or the include which defines it
|
||||
bool m_preview { false }; // the node was made by the editor to show an include it placed or changed, and is replaced when the include changes
|
||||
scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any
|
||||
// include of an *.inc template which defines the node; 0 if the node is defined directly by a scenery layer file.
|
||||
// such nodes can't be rewritten one by one on scenery save. set only for scenery opened for editing
|
||||
|
||||
@@ -497,6 +497,51 @@ std::pair<int, int> state_manager::preview_include(std::string const &Directive,
|
||||
return m_serializer.preview_include(Directive, Context, Layer, Instance);
|
||||
}
|
||||
|
||||
std::pair<int, int> state_manager::rebuild_include(scene::instance_handle const Instance, std::vector<TAnimModel *> &Retired) {
|
||||
if (false == scene::Layers.tracked(Instance)) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
auto const &included { scene::Layers.instance(Instance) };
|
||||
// the models the include was loaded with don't match it anymore.
|
||||
// NOTE: done ahead of retiring the models made by the editor, which are told from the loaded ones by their preview flag
|
||||
scene::Layers.rebuilt(Instance);
|
||||
for (auto *model : Instances.sequence()) {
|
||||
if (model == nullptr || model->m_instance != Instance || false == model->m_preview) {
|
||||
continue;
|
||||
}
|
||||
if (false == included.removed) {
|
||||
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
|
||||
}
|
||||
Region->erase(model);
|
||||
scene::Hierarchy.erase(model->uuid.to_string());
|
||||
model->visible(false);
|
||||
model->m_preview = false;
|
||||
Retired.emplace_back(model);
|
||||
}
|
||||
if (included.dead) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
auto const layer { scene::Layers.resolve(included.layer) };
|
||||
auto const result { m_serializer.preview_include(scene::Layers.directive(Instance), included.context, layer, Instance) };
|
||||
// what's shown for an include which is out of sight at the moment goes out of sight as well
|
||||
auto const layerhidden { scene::Layers.valid(layer) && false == scene::Layers.layer(layer).visible };
|
||||
if (included.removed || layerhidden) {
|
||||
for (auto *model : Instances.sequence()) {
|
||||
if (model == nullptr || model->m_instance != Instance || false == model->m_preview) {
|
||||
continue;
|
||||
}
|
||||
if (included.removed) {
|
||||
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
|
||||
}
|
||||
if (model->visible()) {
|
||||
model->visible(false);
|
||||
model->m_layerhidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void state_manager::delete_model(TAnimModel *model) {
|
||||
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
|
||||
scene::Layers.forget(model);
|
||||
|
||||
@@ -46,6 +46,11 @@ public:
|
||||
// creates model instances defined by an include directive placed in the scenery editor
|
||||
std::pair<int, int>
|
||||
preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance);
|
||||
// replaces models shown for specified include with the ones defined by its current directive. Retired: receives
|
||||
// the replaced models made by the editor. they're out of the scene already, but the renderer can still hold on
|
||||
// to them for a few frames; pass them to Instances.purge() afterwards
|
||||
std::pair<int, int>
|
||||
rebuild_include(scene::instance_handle Instance, std::vector<TAnimModel *> &Retired);
|
||||
// delete TAnimModel instance
|
||||
void
|
||||
delete_model(TAnimModel *model);
|
||||
|
||||
@@ -1451,12 +1451,9 @@ std::pair<int, int> state_serializer::preview_include(std::string const &Directi
|
||||
nodedata.instance = Instance;
|
||||
auto *instance { (nodedata.type == "model" && nodedata.range_min >= 0.0) ? deserialize_model(parser, scratch, nodedata) : nullptr };
|
||||
if (instance != nullptr) {
|
||||
// same as for a model loaded with the scenery
|
||||
if (instance->Model() != nullptr) {
|
||||
for (auto const &smokesource : instance->Model()->smoke_sources()) {
|
||||
Particles.insert(smokesource.first, instance, smokesource.second);
|
||||
}
|
||||
}
|
||||
// NOTE: unlike for a model loaded with the scenery, smoke sources of the model aren't set up.
|
||||
// they'd be left with a dangling owner when the model is replaced after a change of the include
|
||||
instance->m_preview = true;
|
||||
simulation::Instances.insert(instance);
|
||||
simulation::Region->insert(instance);
|
||||
scene::Hierarchy[instance->uuid.to_string()] = instance;
|
||||
|
||||
@@ -46,7 +46,7 @@ public:
|
||||
TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position);
|
||||
// create new eventlauncher from node stirng
|
||||
TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position);
|
||||
// creates model instances defined by an include directive placed in the scenery editor, to show what was placed.
|
||||
// creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome.
|
||||
// everything else the included template defines is left out, and takes effect when the saved scenery is loaded.
|
||||
// Context: placement in effect at the location of the directive. returns: number of created models and of skipped statements
|
||||
std::pair<int, int> preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance);
|
||||
|
||||
Reference in New Issue
Block a user