mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 06:19:19 +02:00
fix(editor): stuck brush after clicks on UI; add polygon area fill (#191)
This commit is contained in:
@@ -24,6 +24,8 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "model/Model3d.h"
|
||||
#include "utilities/Float3d.h"
|
||||
#include "scene/scene.h"
|
||||
#include "utilities/parser.h"
|
||||
#include "utilities/utilities.h"
|
||||
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
@@ -58,6 +60,14 @@ namespace
|
||||
return state == GLFW_PRESS;
|
||||
}
|
||||
|
||||
// true when the most recent left button press landed in the viewport rather than on an ImGui window.
|
||||
// valid once ImGui has processed the press (i.e. from mode update onwards), so it's also used to
|
||||
// discard deferred pick callbacks of a press that leaked past the application's UI filter
|
||||
inline bool viewport_click()
|
||||
{
|
||||
return false == ImGui::GetIO().MouseDownOwned[0];
|
||||
}
|
||||
|
||||
// tests whether the vertical line through (Px,Pz) passes over triangle abc; if so returns the
|
||||
// surface height at that point through OutY. used by the "snap to ground" (END) feature.
|
||||
inline bool triangle_height_at(glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c,
|
||||
@@ -117,6 +127,158 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// world transform of a model instance, matching the renderer: translate * rotateY * rotateX * rotateZ * scale
|
||||
glm::dmat4 instance_matrix(TAnimModel const &Model)
|
||||
{
|
||||
glm::dmat4 rootm(1.0);
|
||||
rootm = glm::translate(rootm, Model.location());
|
||||
glm::vec3 const angles = Model.Angles();
|
||||
if (angles.y != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.y)), glm::dvec3(0.0, 1.0, 0.0));
|
||||
if (angles.x != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.x)), glm::dvec3(1.0, 0.0, 0.0));
|
||||
if (angles.z != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.z)), glm::dvec3(0.0, 0.0, 1.0));
|
||||
return glm::scale(rootm, glm::dvec3(Model.Scale()));
|
||||
}
|
||||
|
||||
// even-odd test of a point against a polygon outline projected onto the XZ plane
|
||||
bool point_in_polygon_xz(std::vector<glm::dvec3> const &Polygon, double const X, double const Z)
|
||||
{
|
||||
bool inside = false;
|
||||
for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++)
|
||||
{
|
||||
auto const &a = Polygon[i];
|
||||
auto const &b = Polygon[j];
|
||||
if ((a.z > Z) != (b.z > Z) && X < (b.x - a.x) * (Z - a.z) / (b.z - a.z) + a.x)
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// area of a polygon outline projected onto the XZ plane (shoelace formula)
|
||||
double polygon_area_xz(std::vector<glm::dvec3> const &Polygon)
|
||||
{
|
||||
if (Polygon.size() < 3)
|
||||
return 0.0;
|
||||
double sum = 0.0;
|
||||
for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++)
|
||||
sum += Polygon[j].x * Polygon[i].z - Polygon[i].x * Polygon[j].z;
|
||||
return std::abs(sum) * 0.5;
|
||||
}
|
||||
|
||||
// short readable name of a node template (model file and texture), like the node bank shows it
|
||||
std::string template_label(std::string const &Template)
|
||||
{
|
||||
cParser tokenizer(Template);
|
||||
tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation
|
||||
auto model{tokenizer.getToken<std::string>(false)};
|
||||
auto texture{tokenizer.getToken<std::string>(false)};
|
||||
replace_slashes(model);
|
||||
erase_extension(model);
|
||||
replace_slashes(texture);
|
||||
return texture == "none" || texture.empty() ? model : model + " (" + texture + ")";
|
||||
}
|
||||
|
||||
// world triangles bucketed on a uniform XZ grid, for many ground height queries over one area
|
||||
class triangle_grid
|
||||
{
|
||||
public:
|
||||
triangle_grid(std::vector<world_triangle> Triangles, glm::dvec2 const &Min, glm::dvec2 const &Max) : m_triangles(std::move(Triangles)), m_min(Min)
|
||||
{
|
||||
glm::dvec2 const extent = glm::max(Max - Min, glm::dvec2(1.0));
|
||||
// ~8 m cells, coarser for big areas so large terrain triangles don't flood the grid
|
||||
m_cellsize = std::max(8.0, std::max(extent.x, extent.y) / 128.0);
|
||||
m_columns = static_cast<int>(std::ceil(extent.x / m_cellsize)) + 1;
|
||||
m_rows = static_cast<int>(std::ceil(extent.y / m_cellsize)) + 1;
|
||||
m_cells.resize(static_cast<std::size_t>(m_columns) * m_rows);
|
||||
|
||||
for (std::uint32_t idx = 0; idx < m_triangles.size(); ++idx)
|
||||
{
|
||||
auto const &t = m_triangles[idx];
|
||||
int const c0 = column(std::min({t[0].x, t[1].x, t[2].x}));
|
||||
int const c1 = column(std::max({t[0].x, t[1].x, t[2].x}));
|
||||
int const r0 = row(std::min({t[0].z, t[1].z, t[2].z}));
|
||||
int const r1 = row(std::max({t[0].z, t[1].z, t[2].z}));
|
||||
for (int r = r0; r <= r1; ++r)
|
||||
for (int c = c0; c <= c1; ++c)
|
||||
m_cells[static_cast<std::size_t>(r) * m_columns + c].push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one
|
||||
bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const
|
||||
{
|
||||
bool found = false;
|
||||
for (auto const idx : m_cells[static_cast<std::size_t>(row(Z)) * m_columns + column(X)])
|
||||
{
|
||||
auto const &t = m_triangles[idx];
|
||||
double y;
|
||||
if (triangle_height_at(t[0], t[1], t[2], X, Z, y) && y <= Ceiling && (!found || y > OutY))
|
||||
{
|
||||
OutY = y;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private:
|
||||
int column(double const X) const
|
||||
{
|
||||
return std::clamp(static_cast<int>(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1);
|
||||
}
|
||||
int row(double const Z) const
|
||||
{
|
||||
return std::clamp(static_cast<int>(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1);
|
||||
}
|
||||
|
||||
std::vector<world_triangle> m_triangles;
|
||||
std::vector<std::vector<std::uint32_t>> m_cells;
|
||||
glm::dvec2 m_min;
|
||||
double m_cellsize{8.0};
|
||||
int m_columns{1};
|
||||
int m_rows{1};
|
||||
};
|
||||
|
||||
// appends triangles of a shape node which overlap specified XZ rectangle
|
||||
void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out)
|
||||
{
|
||||
auto const &data = Shape.data();
|
||||
if (data.area.radius >= 0.0f)
|
||||
{
|
||||
double const r = data.area.radius;
|
||||
if (data.area.center.x + r < Min.x || data.area.center.x - r > Max.x || data.area.center.z + r < Min.y || data.area.center.z - r > Max.y)
|
||||
return;
|
||||
}
|
||||
|
||||
auto const add = [&](glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c) {
|
||||
if (std::max({a.x, b.x, c.x}) < Min.x || std::min({a.x, b.x, c.x}) > Max.x || std::max({a.z, b.z, c.z}) < Min.y || std::min({a.z, b.z, c.z}) > Max.y)
|
||||
return;
|
||||
Out.push_back({a, b, c});
|
||||
};
|
||||
|
||||
if (false == data.vertices.empty())
|
||||
{
|
||||
for (std::size_t i = 0; i + 2 < data.vertices.size(); i += 3)
|
||||
add(data.vertices[i].position, data.vertices[i + 1].position, data.vertices[i + 2].position);
|
||||
return;
|
||||
}
|
||||
// once the shape is uploaded its world-space source is released; the renderer keeps an origin-relative copy
|
||||
if (data.geometry.bank == 0 && data.geometry.chunk == 0)
|
||||
return;
|
||||
auto const &verts = GfxRenderer->Vertices(data.geometry);
|
||||
auto const &indices = GfxRenderer->Indices(data.geometry);
|
||||
auto const to_world = [&](gfx::basic_vertex const &v) { return data.origin + glm::dvec3(v.position); };
|
||||
if (false == indices.empty())
|
||||
{
|
||||
for (std::size_t i = 0; i + 2 < indices.size(); i += 3)
|
||||
add(to_world(verts[indices[i]]), to_world(verts[indices[i + 1]]), to_world(verts[indices[i + 2]]));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (std::size_t i = 0; i + 2 < verts.size(); i += 3)
|
||||
add(to_world(verts[i]), to_world(verts[i + 1]), to_world(verts[i + 2]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool editor_mode::editormode_input::init()
|
||||
@@ -306,32 +468,38 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button)
|
||||
|
||||
GfxRenderer->Pick_Node_Callback(
|
||||
[this, mode, rotation_mode, fixed_rotation_value, Action, Button](scene::basic_node * /*node*/) {
|
||||
// picks are resolved a few frames later; drop the ones queued before the button went up,
|
||||
// or for a press that turned out to belong to the UI
|
||||
if (!mouseHold || !viewport_click())
|
||||
return;
|
||||
|
||||
const std::string *src = ui()->get_active_node_template();
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
std::string name = "editor_";
|
||||
|
||||
glm::dvec3 newPos = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
|
||||
double distance = glm::distance(newPos, oldPos);
|
||||
if (distance < ui()->getSpacing())
|
||||
// spacing is measured in world space: the mouse offset is camera-relative and the camera may move mid-stroke
|
||||
glm::dvec3 const newPos = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position());
|
||||
if (m_brush_has_last && glm::distance(newPos, oldPos) < ui()->getSpacing())
|
||||
return;
|
||||
|
||||
TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + newPos);
|
||||
TAnimModel *cloned = simulation::State.create_model(*src, name, newPos);
|
||||
oldPos = newPos;
|
||||
m_brush_has_last = true;
|
||||
if (!cloned)
|
||||
return;
|
||||
|
||||
std::string new_name = "editor_" + cloned->uuid.to_string();
|
||||
|
||||
cloned->m_name = new_name;
|
||||
|
||||
apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value);
|
||||
|
||||
std::string as_text;
|
||||
cloned->export_as_text(as_text);
|
||||
push_snapshot(cloned, EditorSnapshot::Action::Add, as_text);
|
||||
|
||||
m_node = cloned;
|
||||
apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value);
|
||||
ui()->set_node(m_node);
|
||||
});
|
||||
}
|
||||
@@ -439,6 +607,9 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node)
|
||||
if (s.node_ptr == node)
|
||||
s.node_ptr = nullptr;
|
||||
}
|
||||
|
||||
// deleted nodes also drop out of the "undo last fill" set
|
||||
m_fill_last.erase(std::remove(m_fill_last.begin(), m_fill_last.end(), node), m_fill_last.end());
|
||||
}
|
||||
|
||||
void editor_mode::undo_last()
|
||||
@@ -626,6 +797,19 @@ bool editor_mode::update()
|
||||
Application.set_cursor(GLFW_CURSOR_NORMAL);
|
||||
}
|
||||
|
||||
// same for the left button (brush / sculpt / click placement). the UI filter in the application
|
||||
// uses io.WantCaptureMouse from the previous frame, so a press on a panel the cursor has just
|
||||
// moved onto can leak into the viewport, and its release is then swallowed by the UI; the brush
|
||||
// would keep painting with the button up. by now ImGui has processed this frame's input, so
|
||||
// MouseDown is the real button state and MouseDownOwned tells whether the press hit the UI.
|
||||
// (pick callbacks queued by such a press are discarded separately, see viewport_click())
|
||||
if (mouseHold && (!ImGui::GetIO().MouseDown[0] || !viewport_click()))
|
||||
{
|
||||
mouseHold = false;
|
||||
m_dragging = false;
|
||||
m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE);
|
||||
}
|
||||
|
||||
// fixed step render time routines (50 Hz)
|
||||
fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable
|
||||
while (fTime50Hz >= 1.0 / 50.0)
|
||||
@@ -708,6 +892,13 @@ bool editor_mode::update()
|
||||
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
||||
render_gizmo();
|
||||
|
||||
// --- area fill: outline overlay and settings, while the mode is active ---
|
||||
if (ui()->mode() == nodebank_panel::FILL)
|
||||
{
|
||||
draw_area_fill_outline();
|
||||
render_area_fill();
|
||||
}
|
||||
|
||||
// --- ImGui: Editor Settings & History windows ---
|
||||
if(m_settings_open)
|
||||
render_settings();
|
||||
@@ -1097,18 +1288,8 @@ void editor_mode::capture_terrain()
|
||||
return;
|
||||
}
|
||||
|
||||
// instance world transform, matching the renderer: translate * rotateY * rotateX * rotateZ * scale
|
||||
glm::dmat4 rootm(1.0);
|
||||
rootm = glm::translate(rootm, model->location());
|
||||
glm::vec3 const angles = model->Angles();
|
||||
if (angles.y != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.y)), glm::dvec3(0.0, 1.0, 0.0));
|
||||
if (angles.x != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.x)), glm::dvec3(1.0, 0.0, 0.0));
|
||||
if (angles.z != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.z)), glm::dvec3(0.0, 0.0, 1.0));
|
||||
glm::vec3 const scale = model->Scale();
|
||||
rootm = glm::scale(rootm, glm::dvec3(scale));
|
||||
|
||||
std::vector<world_triangle> tris;
|
||||
gather_submodel_triangles(model->pModel->Root, rootm, tris);
|
||||
gather_submodel_triangles(model->pModel->Root, instance_matrix(*model), tris);
|
||||
if (tris.empty())
|
||||
{
|
||||
WriteLog("Editor: selected model has no readable geometry to capture", logtype::generic);
|
||||
@@ -1174,6 +1355,366 @@ void editor_mode::capture_terrain()
|
||||
simulation::State.delete_model(model);
|
||||
}
|
||||
|
||||
void editor_mode::add_area_fill_point()
|
||||
{
|
||||
// like the other placement tools, the point comes from the depth buffer under the cursor
|
||||
// (ground geometry only), relative to the camera
|
||||
glm::dvec3 const offset = GfxRenderer->Mouse_Position();
|
||||
if (glm::length(offset) < 1e-3)
|
||||
return; // no ground under the cursor (yet)
|
||||
|
||||
m_fill_points.push_back(Camera.Pos + offset);
|
||||
}
|
||||
|
||||
void editor_mode::undo_last_area_fill()
|
||||
{
|
||||
// nullify_history_pointers() removes deleted nodes from m_fill_last, so work on a copy
|
||||
auto const created = std::move(m_fill_last);
|
||||
m_fill_last.clear();
|
||||
|
||||
for (TAnimModel *model : created)
|
||||
{
|
||||
if (model == nullptr)
|
||||
continue;
|
||||
if (m_node == model)
|
||||
{
|
||||
m_node = nullptr;
|
||||
ui()->set_node(nullptr);
|
||||
}
|
||||
nullify_history_pointers(model);
|
||||
remove_from_hierarchy(model);
|
||||
simulation::State.delete_model(model);
|
||||
}
|
||||
m_fill_status = "Removed " + std::to_string(created.size()) + " objects of the last fill";
|
||||
}
|
||||
|
||||
void editor_mode::run_area_fill()
|
||||
{
|
||||
// limits keeping a single click from freezing the editor for too long
|
||||
std::size_t constexpr max_objects = 20000;
|
||||
// large model instances (e.g. terrain tiles) are treated as ground, smaller ones (houses, trees) are not
|
||||
float constexpr terrain_model_radius = 50.0f;
|
||||
// ground is searched up to this far above the height interpolated from the outline points,
|
||||
// so hills inside the outline are found but bridges or roofs high above it are not
|
||||
double constexpr height_tolerance = 50.0;
|
||||
|
||||
if (m_fill_points.size() < 3)
|
||||
{
|
||||
m_fill_status = "Outline needs at least 3 points";
|
||||
return;
|
||||
}
|
||||
|
||||
// model package
|
||||
std::vector<std::string> package;
|
||||
if (m_fill_package == 0)
|
||||
package = m_fill_custom;
|
||||
else
|
||||
package = ui()->nodebank_group_templates(static_cast<std::size_t>(m_fill_package - 1));
|
||||
package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end());
|
||||
if (package.empty())
|
||||
{
|
||||
m_fill_status = "Selected model package is empty";
|
||||
return;
|
||||
}
|
||||
|
||||
// area bounds
|
||||
glm::dvec2 bmin(std::numeric_limits<double>::max());
|
||||
glm::dvec2 bmax(-std::numeric_limits<double>::max());
|
||||
for (auto const &p : m_fill_points)
|
||||
{
|
||||
bmin = glm::min(bmin, glm::dvec2(p.x, p.z));
|
||||
bmax = glm::max(bmax, glm::dvec2(p.x, p.z));
|
||||
}
|
||||
double const area = polygon_area_xz(m_fill_points);
|
||||
auto const target = static_cast<std::size_t>(std::min<double>(std::round(area / 10000.0 * std::max(0.0f, m_fill_density)), max_objects));
|
||||
if (target == 0)
|
||||
{
|
||||
m_fill_status = "Density too low for this area, nothing to place";
|
||||
return;
|
||||
}
|
||||
|
||||
// scatter points: random samples inside the outline, rejecting the ones closer than the minimal spacing
|
||||
// to an already accepted point (checked on a hashed grid with cell size == spacing)
|
||||
double const spacing = std::max(0.0f, m_fill_min_spacing);
|
||||
std::unordered_map<std::int64_t, std::vector<std::size_t>> occupancy;
|
||||
auto const cell_key = [](std::int64_t const X, std::int64_t const Z) { return (X << 32) ^ (Z & 0xffffffff); };
|
||||
std::vector<glm::dvec2> points;
|
||||
points.reserve(target);
|
||||
for (std::size_t attempt = 0; attempt < target * 30 && points.size() < target; ++attempt)
|
||||
{
|
||||
glm::dvec2 const p(LocalRandom(bmin.x, bmax.x), LocalRandom(bmin.y, bmax.y));
|
||||
if (false == point_in_polygon_xz(m_fill_points, p.x, p.y))
|
||||
continue;
|
||||
if (spacing > 0.0)
|
||||
{
|
||||
auto const cx = static_cast<std::int64_t>(std::floor(p.x / spacing));
|
||||
auto const cz = static_cast<std::int64_t>(std::floor(p.y / spacing));
|
||||
bool free = true;
|
||||
for (std::int64_t dz = -1; dz <= 1 && free; ++dz)
|
||||
for (std::int64_t dx = -1; dx <= 1 && free; ++dx)
|
||||
{
|
||||
auto const lookup = occupancy.find(cell_key(cx + dx, cz + dz));
|
||||
if (lookup == occupancy.end())
|
||||
continue;
|
||||
for (auto const idx : lookup->second)
|
||||
if (glm::distance(points[idx], p) < spacing)
|
||||
{
|
||||
free = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!free)
|
||||
continue;
|
||||
occupancy[cell_key(cx, cz)].push_back(points.size());
|
||||
}
|
||||
points.push_back(p);
|
||||
}
|
||||
|
||||
// ground geometry within the area. shapes hold the (legacy) terrain, large instances the terrain tiles
|
||||
std::vector<world_triangle> triangles;
|
||||
glm::dvec2 const center = (bmin + bmax) * 0.5;
|
||||
float const radius = static_cast<float>(glm::length(bmax - bmin) * 0.5);
|
||||
auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad
|
||||
for (auto *section : sections)
|
||||
{
|
||||
for (auto const &shape : section->m_shapes)
|
||||
gather_shape_triangles(shape, bmin, bmax, triangles);
|
||||
for (auto &cell : section->m_cells)
|
||||
{
|
||||
double const r = cell.m_area.radius;
|
||||
if (cell.m_area.center.x + r < bmin.x || cell.m_area.center.x - r > bmax.x || cell.m_area.center.z + r < bmin.y || cell.m_area.center.z - r > bmax.y)
|
||||
continue;
|
||||
for (auto const &shape : cell.m_shapesopaque)
|
||||
gather_shape_triangles(shape, bmin, bmax, triangles);
|
||||
if (false == m_fill_models_as_ground)
|
||||
continue;
|
||||
for (auto *instance : cell.m_instancesopaque)
|
||||
{
|
||||
if (instance == nullptr || instance->Model() == nullptr || instance->radius() < terrain_model_radius)
|
||||
continue;
|
||||
std::vector<world_triangle> modeltriangles;
|
||||
gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles);
|
||||
for (auto const &t : modeltriangles)
|
||||
if (std::max({t[0].x, t[1].x, t[2].x}) >= bmin.x && std::min({t[0].x, t[1].x, t[2].x}) <= bmax.x && std::max({t[0].z, t[1].z, t[2].z}) >= bmin.y &&
|
||||
std::min({t[0].z, t[1].z, t[2].z}) <= bmax.y)
|
||||
triangles.push_back(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
triangle_grid const ground(std::move(triangles), bmin, bmax);
|
||||
auto const terrains = active_terrains();
|
||||
|
||||
auto const rotation_mode = m_fill_random_rotation ? functions_panel::RANDOM : ui()->rot_mode();
|
||||
auto const fixed_rotation_value = ui()->rot_val();
|
||||
float const scalemin = std::max(0.01f, std::min(m_fill_scale_min, m_fill_scale_max));
|
||||
float const scalemax = std::max(0.01f, std::max(m_fill_scale_min, m_fill_scale_max));
|
||||
|
||||
std::vector<TAnimModel *> created;
|
||||
created.reserve(points.size());
|
||||
std::size_t unsupported = 0;
|
||||
for (auto const &p : points)
|
||||
{
|
||||
// reference height: the outline points, weighted by inverse squared distance
|
||||
double weights = 0.0, reference = 0.0;
|
||||
for (auto const &v : m_fill_points)
|
||||
{
|
||||
double const w = 1.0 / std::max(1e-6, glm::length2(glm::dvec2(v.x, v.z) - p));
|
||||
weights += w;
|
||||
reference += w * v.y;
|
||||
}
|
||||
reference /= weights;
|
||||
double const ceiling = reference + height_tolerance;
|
||||
|
||||
double y = -std::numeric_limits<double>::max();
|
||||
bool found = ground.height_at(p.x, p.y, ceiling, y);
|
||||
for (editor_terrain *terrain : terrains)
|
||||
{
|
||||
if (!terrain->contains(p.x, p.y))
|
||||
continue;
|
||||
double const h = terrain->height_at(p.x, p.y);
|
||||
if (h <= ceiling && (!found || h > y))
|
||||
{
|
||||
y = h;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
y = reference;
|
||||
++unsupported;
|
||||
}
|
||||
|
||||
glm::dvec3 const location(p.x, y, p.y);
|
||||
if (!simulation::Region->point_inside(location))
|
||||
continue;
|
||||
|
||||
auto const &src = package[package.size() > 1 ? std::min(package.size() - 1, static_cast<std::size_t>(LocalRandom(0.0, static_cast<double>(package.size())))) : 0];
|
||||
TAnimModel *cloned = simulation::State.create_model(src, "editor_", location);
|
||||
if (!cloned)
|
||||
continue;
|
||||
cloned->m_name = "editor_" + cloned->uuid.to_string();
|
||||
apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value);
|
||||
if (scalemin != 1.0f || scalemax != 1.0f)
|
||||
cloned->Scale(static_cast<float>(LocalRandom(scalemin, scalemax)));
|
||||
created.push_back(cloned);
|
||||
}
|
||||
|
||||
// a new fill replaces the "last fill" undo set; the previous objects stay in the scene
|
||||
m_fill_last = std::move(created);
|
||||
|
||||
m_fill_status = "Placed " + std::to_string(m_fill_last.size()) + " of " + std::to_string(target) + " objects";
|
||||
if (points.size() < target)
|
||||
m_fill_status += " (limited by min. spacing)";
|
||||
if (unsupported > 0)
|
||||
m_fill_status += ", " + std::to_string(unsupported) + " without ground found (height interpolated)";
|
||||
WriteLog("Editor: area fill - " + m_fill_status, logtype::generic);
|
||||
}
|
||||
|
||||
void editor_mode::draw_area_fill_outline() const
|
||||
{
|
||||
if (m_fill_points.empty())
|
||||
return;
|
||||
|
||||
// same camera-relative view and clean perspective the transform gizmo uses
|
||||
ImGuiIO const &io = ImGui::GetIO();
|
||||
glm::mat4 const view = GfxRenderer->Camera_View_Matrix();
|
||||
glm::dvec3 const camerapos = GfxRenderer->Camera_Position();
|
||||
float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor);
|
||||
float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f;
|
||||
glm::mat4 const viewprojection = glm::perspective(fovy, aspect, 0.1f, 10000.0f) * view;
|
||||
|
||||
auto const to_clip = [&](glm::dvec3 const &Point) { return viewprojection * glm::vec4(glm::vec3(Point - camerapos), 1.0f); };
|
||||
auto const to_screen = [&](glm::vec4 const &Clip) {
|
||||
return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * io.DisplaySize.x, (0.5f - Clip.y / Clip.w * 0.5f) * io.DisplaySize.y);
|
||||
};
|
||||
float constexpr nearw = 0.1f;
|
||||
|
||||
ImDrawList *drawlist = ImGui::GetBackgroundDrawList();
|
||||
ImU32 const edgecolor = IM_COL32(255, 200, 40, 230);
|
||||
ImU32 const closingcolor = IM_COL32(255, 200, 40, 110);
|
||||
|
||||
std::size_t const count = m_fill_points.size();
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
bool const closing = (i + 1 == count);
|
||||
if (closing && count < 3)
|
||||
break;
|
||||
glm::vec4 a = to_clip(m_fill_points[i]);
|
||||
glm::vec4 b = to_clip(m_fill_points[(i + 1) % count]);
|
||||
// clip the edge against the near plane, so points behind the camera don't flip across the screen
|
||||
if (a.w < nearw && b.w < nearw)
|
||||
continue;
|
||||
if (a.w < nearw)
|
||||
a = glm::mix(a, b, (nearw - a.w) / (b.w - a.w));
|
||||
else if (b.w < nearw)
|
||||
b = glm::mix(b, a, (nearw - b.w) / (a.w - b.w));
|
||||
drawlist->AddLine(to_screen(a), to_screen(b), closing ? closingcolor : edgecolor, 2.0f);
|
||||
}
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
glm::vec4 const c = to_clip(m_fill_points[i]);
|
||||
if (c.w < nearw)
|
||||
continue;
|
||||
drawlist->AddCircleFilled(to_screen(c), i == 0 ? 6.0f : 4.0f, i == 0 ? IM_COL32(255, 90, 40, 255) : edgecolor);
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::render_area_fill()
|
||||
{
|
||||
ImGui::Begin("Area fill", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
|
||||
ImGui::TextUnformatted("LMB: add outline point, Backspace: remove last point");
|
||||
double const area = polygon_area_xz(m_fill_points);
|
||||
ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0);
|
||||
if (ImGui::Button("Remove last point") && !m_fill_points.empty())
|
||||
m_fill_points.pop_back();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear outline"))
|
||||
m_fill_points.clear();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// model package: a node bank group, or a custom set assembled from node bank templates
|
||||
auto const groups = ui()->nodebank_groups();
|
||||
m_fill_package = std::clamp(m_fill_package, 0, static_cast<int>(groups.size()));
|
||||
std::string const current = m_fill_package == 0 ? std::string("Custom set") : groups[m_fill_package - 1];
|
||||
ImGui::SetNextItemWidth(250.0f);
|
||||
if (ImGui::BeginCombo("Model package", current.c_str()))
|
||||
{
|
||||
if (ImGui::Selectable("Custom set", m_fill_package == 0))
|
||||
m_fill_package = 0;
|
||||
for (int i = 0; i < static_cast<int>(groups.size()); ++i)
|
||||
{
|
||||
auto const label = groups[i] + "##fillgroup" + std::to_string(i);
|
||||
if (ImGui::Selectable(label.c_str(), m_fill_package == i + 1))
|
||||
m_fill_package = i + 1;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
if (m_fill_package == 0)
|
||||
{
|
||||
ImGui::BeginChild("fill_custom_set", ImVec2(350.0f, 110.0f), true);
|
||||
for (int i = 0; i < static_cast<int>(m_fill_custom.size()); ++i)
|
||||
{
|
||||
auto const label = template_label(m_fill_custom[i]) + "##fillcustom" + std::to_string(i);
|
||||
if (ImGui::Selectable(label.c_str(), m_fill_custom_idx == i))
|
||||
m_fill_custom_idx = i;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
if (ImGui::Button("Add selected template"))
|
||||
{
|
||||
// the node bank selection, not a random pick from the brush set
|
||||
auto const *src = ui()->get_active_node_template(true);
|
||||
if (src && !src->empty())
|
||||
m_fill_custom.push_back(*src);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove") && m_fill_custom_idx >= 0 && m_fill_custom_idx < static_cast<int>(m_fill_custom.size()))
|
||||
{
|
||||
m_fill_custom.erase(m_fill_custom.begin() + m_fill_custom_idx);
|
||||
m_fill_custom_idx = -1;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear set"))
|
||||
{
|
||||
m_fill_custom.clear();
|
||||
m_fill_custom_idx = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text("Templates in group: %zu", ui()->nodebank_group_templates(static_cast<std::size_t>(m_fill_package - 1)).size());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Density (objects/ha)", &m_fill_density, 10.0f, 100.0f, "%.1f");
|
||||
m_fill_density = std::max(0.0f, m_fill_density);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Min. spacing (m)", &m_fill_min_spacing, 0.5f, 2.0f, "%.1f");
|
||||
m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing);
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f");
|
||||
ImGui::Checkbox("Random rotation (off: Functions panel setting)", &m_fill_random_rotation);
|
||||
ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground);
|
||||
|
||||
ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density));
|
||||
|
||||
if (ImGui::Button("Fill area"))
|
||||
run_area_fill();
|
||||
ImGui::SameLine();
|
||||
std::string const undolabel = "Undo last fill (" + std::to_string(m_fill_last.size()) + ")";
|
||||
if (ImGui::Button(undolabel.c_str()) && !m_fill_last.empty())
|
||||
undo_last_area_fill();
|
||||
|
||||
if (!m_fill_status.empty())
|
||||
ImGui::TextUnformatted(m_fill_status.c_str());
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::render_gizmo()
|
||||
{
|
||||
// the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse
|
||||
@@ -1381,6 +1922,7 @@ void editor_mode::exit()
|
||||
|
||||
g_redo.clear();
|
||||
m_history.clear();
|
||||
m_fill_last.clear();
|
||||
|
||||
// drop selection so a stale/dangling node pointer isn't used on the next editor session
|
||||
m_node = nullptr;
|
||||
@@ -1496,6 +2038,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
}
|
||||
break;
|
||||
|
||||
case GLFW_KEY_BACKSPACE:
|
||||
// area fill: remove the last outline point
|
||||
if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty())
|
||||
m_fill_points.pop_back();
|
||||
break;
|
||||
|
||||
case GLFW_KEY_F:
|
||||
if (is_press(Action))
|
||||
{
|
||||
@@ -1565,11 +2113,30 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
if (is_press(Action))
|
||||
{
|
||||
mouseHold = true;
|
||||
m_node = nullptr;
|
||||
m_brush_has_last = false; // every brush stroke starts with a placement
|
||||
|
||||
// delegate node picking behaviour depending on current panel mode
|
||||
// in area fill mode the left button only adds outline points, it doesn't touch the selection
|
||||
if (mode == nodebank_panel::FILL)
|
||||
{
|
||||
GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) {
|
||||
if (viewport_click())
|
||||
add_area_fill_point();
|
||||
});
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
|
||||
// delegate node picking behaviour depending on current panel mode.
|
||||
// NOTE: the pick is resolved a few frames later, often after the button was already released,
|
||||
// so the callback mustn't depend on the button still being held
|
||||
GfxRenderer->Pick_Node_Callback(
|
||||
[this, mode, rotation_mode, fixed_rotation_value](scene::basic_node *node) {
|
||||
// the press turned out to be meant for the UI
|
||||
if (!viewport_click())
|
||||
return;
|
||||
|
||||
m_node = nullptr;
|
||||
|
||||
// ignore picks that are beyond allowed placement distance
|
||||
if (node) {
|
||||
double const dist = glm::distance(node->location(), glm::dvec3{Global.pCamera.Pos});
|
||||
@@ -1578,9 +2145,6 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
}
|
||||
if (mode == nodebank_panel::MODIFY)
|
||||
{
|
||||
if (!m_dragging)
|
||||
return;
|
||||
|
||||
m_node = node;
|
||||
ui()->set_node(m_node);
|
||||
}
|
||||
@@ -1607,19 +2171,16 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
if (!cloned)
|
||||
return;
|
||||
|
||||
// record addition for undo
|
||||
std::string as_text;
|
||||
std::string new_name = "editor_" + cloned->uuid.to_string();
|
||||
|
||||
cloned->m_name = new_name;
|
||||
apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value);
|
||||
|
||||
// record addition for undo (after rotation, so a redo recreates the same thing)
|
||||
std::string as_text;
|
||||
cloned->export_as_text(as_text);
|
||||
push_snapshot(cloned, EditorSnapshot::Action::Add, as_text);
|
||||
|
||||
if (!m_dragging)
|
||||
return;
|
||||
|
||||
m_node = cloned;
|
||||
apply_rotation_for_new_node(m_node, rotation_mode, fixed_rotation_value);
|
||||
ui()->set_node(m_node);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user