17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 02:49:19 +02:00

feat(roads): right of way and lane turns at junctions, road weights, kerbs, medians and forking roads, banks to the ground, roundabouts, pedestrian crossings, spawn sets by count; fix(editor): model skin export, placement on the ground under the orthophoto

This commit is contained in:
2026-10-04 01:55:35 +02:00
parent 3ce9102da0
commit 27a94509c4
17 changed files with 2198 additions and 244 deletions

View File

@@ -476,7 +476,10 @@ editor_mode::editor_mode() {
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
ui()->set_track_options([this]() { render_track_ui(); });
ui()->set_menu_options([this]() { render_road_menu(); });
ui()->set_menu_options([this]() {
render_object_menu();
render_road_menu();
});
m_roadtool.settings.normalize();
road_select(nullptr);
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
@@ -744,7 +747,7 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button)
std::string name = "editor_";
TAnimModel *cloned = simulation::State.create_model(*src, name, newPos);
TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(newPos));
oldPos = newPos;
m_brush_has_last = true;
if (!cloned)
@@ -2010,7 +2013,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode,
ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer.", true);
return;
}
glm::dvec3 const location = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
glm::dvec3 const location = placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()));
std::optional<float> yaw;
if (RotationMode == functions_panel::RANDOM)
yaw = static_cast<float>(LocalRandom(0.0, 360.0));
@@ -2148,6 +2151,119 @@ void editor_mode::capture_terrain()
simulation::State.delete_model(model);
}
// the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any
// other surface, so the point can hang over the ground by as much as the orthophoto is lifted, or lie on the flat sheet
// the orthophoto makes where it's not fitted to the ground. what's put in the scenery is to stand on the ground itself
glm::dvec3 editor_mode::placement_on_ground(glm::dvec3 Location)
{
double constexpr reach = 2.0; // the ground is asked about this far around the point
double constexpr slack = 0.5; // a surface this far over the point still is what the cursor landed on
double constexpr height_tolerance = 50.0; // with nothing there or lower, how far up the ground is looked for
float constexpr terrain_model_radius = 50.0f; // the same models count as ground as for the area fill and the orthophoto
glm::dvec2 const bmin{Location.x - reach, Location.z - reach};
glm::dvec2 const bmax{Location.x + reach, Location.z + reach};
std::vector<world_triangle> triangles;
gather_ground_triangles(bmin, bmax, true, terrain_model_radius, triangles);
triangle_grid const ground(std::move(triangles), bmin, bmax);
auto const terrains = active_terrains();
for (double const ceiling : {Location.y + slack, Location.y + height_tolerance})
{
double y = -std::numeric_limits<double>::max();
bool found = ground.height_at(Location.x, Location.z, ceiling, y);
for (editor_terrain *terrain : terrains)
{
if (!terrain->contains(Location.x, Location.z))
continue;
double const h = terrain->height_at(Location.x, Location.z);
if (h <= ceiling && (!found || h > y))
{
y = h;
found = true;
}
}
if (found)
{
Location.y = y;
break;
}
}
// with no ground found the point stays where the cursor landed
return Location;
}
// puts every instance of the model the selected node shows on the ground under it
void editor_mode::drop_model_instances()
{
auto *selected = dynamic_cast<TAnimModel *>(m_node);
if (selected == nullptr || selected->Model() == nullptr)
{
ui()->set_status("Select a model in the scene first", true);
return;
}
if (selected->radius() >= 50.0f)
{
// it'd be put on top of itself
ui()->set_status("\"" + selected->Model()->NameGet() + "\" is large enough to count as ground itself, so there's nothing to put it on", true);
return;
}
std::vector<TAnimModel *> instances;
std::vector<glm::dvec3> points;
int locked = 0;
for (auto *instance : simulation::Instances.sequence())
{
if (instance == nullptr || instance->Model() != selected->Model())
continue;
if (instance->from_template() || false == scene::Layers.editable(instance))
{
// placed by a template, or on a layer which is hidden or locked
++locked;
continue;
}
instances.push_back(instance);
points.push_back(instance->location());
}
// the ground the road tools use: terrain shapes, large models and the terrain made in the editor, without the roads and the orthophoto.
// a point with nothing under it comes back with the height it has
auto const heights = ground_heights(points, true);
int moved = 0;
for (std::size_t idx = 0; idx < instances.size(); ++idx)
{
if (std::abs(heights[idx] - points[idx].y) < 0.001)
continue;
auto *instance = instances[idx];
// one step of the history for each instance
push_snapshot(instance, EditorSnapshot::Action::Move);
auto location = points[idx];
location.y = heights[idx];
simulation::Region->erase(instance);
instance->location(location);
simulation::Region->insert(instance);
instance->mark_dirty();
++moved;
}
std::string status = std::to_string(moved) + " of " + std::to_string(instances.size()) + " instances of \"" + selected->Model()->NameGet() + "\" put on the ground";
if (moved > 0)
status += "; Ctrl+Z takes them back one at a time";
if (locked > 0)
status += ". " + std::to_string(locked) + " left alone: placed by a template, or on a layer which can't be edited";
ui()->set_status(status);
}
void editor_mode::render_object_menu()
{
if (ImGui::BeginMenu("Objects"))
{
auto const *selected = dynamic_cast<TAnimModel const *>(m_node);
if (ImGui::MenuItem("Put all instances of the selected model on the ground", nullptr, false, selected != nullptr && selected->Model() != nullptr))
drop_model_instances();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", "Select a model in the scene. Every instance of the same model file is moved straight up or down\n"
"to the ground under it: terrain, large models and terrain made in the editor.");
ImGui::EndMenu();
}
}
void editor_mode::add_area_fill_point()
{
// like the other placement tools, the point comes from the depth buffer under the cursor
@@ -2156,7 +2272,8 @@ void editor_mode::add_area_fill_point()
if (glm::length(offset) < 1e-3)
return; // no ground under the cursor (yet)
m_fill_points.push_back(Camera.Pos + offset);
// the outline gives the fill the height to look for the ground at, and the height of what it finds no ground for
m_fill_points.push_back(placement_on_ground(Camera.Pos + offset));
}
void editor_mode::undo_last_area_fill()
@@ -3227,7 +3344,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
std::string name = "editor_";
glm::dvec3 mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + mouseOffset);
TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(Camera.Pos + mouseOffset));
if (!cloned)
return;