Files

773 lines
31 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "terrain_generator.h"
#include <algorithm>
#include <cstdio>
#include "debugging/debugging.h"
#include "iplugin.h"
#include "string/string.h"
#include "modulesystem/singletonmodule.h"
#include "typesystem.h"
#include <QWidget>
#include <QDialog>
#include <QEventLoop>
#include <QTimer>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QCheckBox>
#include <QLineEdit>
#include <QPushButton>
#include "gtkutil/spinbox.h"
#include "gtkutil/combobox.h"
#include "scenelib.h"
#include "terrain_math.h"
#include "noise.h"
#include "terrain_engine.h"
#include "brush_builder.h"
#include "ibrush.h"
#include "ientity.h"
#include "ieclass.h"
#include "iscenegraph.h"
#include "iundo.h"
#include "iselection.h"
#include "qerplugin.h"
#include "modulesystem/moduleregistry.h"
namespace terrain_generator
{
QWidget* main_window;
const char* init( void* hApp, void* pMainWidget ){
main_window = static_cast<QWidget*>( pMainWidget );
return "";
}
const char* getName(){
return "TerrainGenerator";
}
const char* getCommandList(){
return "About;Generate Terrain";
}
const char* getCommandTitleList(){
return "";
}
// The entity carrying terrain metadata for a selected instance: the instance's
// node if it is an entity, otherwise its parent (a selected brush's func_group).
static Entity* instance_entity( scene::Instance& instance ){
const scene::Path& p = instance.path();
if ( Node_isEntity( p.top() ) )
return Node_getEntity( p.top().get() );
if ( p.size() > 1 && Node_isEntity( p.parent() ) )
return Node_getEntity( p.parent().get() );
return nullptr;
}
void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush ){
if ( string_equal( command, "About" ) ) {
GlobalRadiant().m_pfnMessageBox( main_window,
"Terrain Generator\n\n"
"Procedural CSG generation tool for id Tech 3 engines.\n\n"
"Developed by vallz and vld",
"About Terrain Generator",
EMessageBoxType::Info, 0 );
return;
}
if ( string_equal( command, "Generate Terrain" ) ) {
// Only one generator dialog at a time. Re-running the command while
// it's open just raises the existing window instead of stacking new ones.
static QDialog* s_active_dialog = nullptr;
if ( s_active_dialog ) {
s_active_dialog->raise();
s_active_dialog->activateWindow();
return;
}
// Query the live selection bounds from the selection system.
// Called at dialog-open time, on mode switch, and on OK — so the
// user can make a selection while the dialog is open and retry.
struct SelBounds { double x0, x1, y0, y1, z0, z1; bool valid; };
auto query_sel = [&]( double min_size = 64.0 ) -> SelBounds {
const AABB& b = GlobalSelectionSystem().getBoundsSelected();
SelBounds s;
s.x0 = b.origin[0] - b.extents[0];
s.x1 = b.origin[0] + b.extents[0];
s.y0 = b.origin[1] - b.extents[1];
s.y1 = b.origin[1] + b.extents[1];
s.z0 = b.origin[2] - b.extents[2];
s.z1 = b.origin[2] + b.extents[2];
s.valid = ( s.x1 - s.x0 >= min_size && s.y1 - s.y0 >= min_size );
return s;
};
// --- Build dialog ---
QDialog dialog( main_window, Qt::Dialog | Qt::WindowCloseButtonHint );
dialog.setWindowTitle( "Terrain Generator" );
s_active_dialog = &dialog;
dialog.setMinimumWidth( 420 );
auto *form = new QFormLayout( &dialog );
// Target mode. Remembered across dialog open/close, same as everything
// else below — regardless of whether there's currently a valid
// selection, so it's predictable rather than silently overridden.
static int s_target_index = 0; // Use Selection
auto *target_combo = new ComboBox;
target_combo->addItem( "Use Selection", 0 );
target_combo->addItem( "Manual Size", 1 );
target_combo->setCurrentIndex( s_target_index );
form->addRow( "Target:", target_combo );
// Selection size — live, read-only labels showing the current selection.
auto *sel_w_label = new QLabel;
auto *sel_l_label = new QLabel;
auto *sel_h_label = new QLabel;
auto refresh_sel_labels = [&](){
const SelBounds s = query_sel();
sel_w_label->setText( s.valid ? QString::number( (int)( s.x1 - s.x0 ) ) : "—" );
sel_l_label->setText( s.valid ? QString::number( (int)( s.y1 - s.y0 ) ) : "—" );
sel_h_label->setText( s.valid ? QString::number( (int)std::max( s.z1 - s.z0, 64.0 ) ) : "—" );
};
refresh_sel_labels();
form->addRow( "Width (X):", sel_w_label );
form->addRow( "Length (Y):", sel_l_label );
form->addRow( "Height (Z):", sel_h_label );
// Manual size inputs (shown in Manual Size mode). Remembers the last
// values used across dialog open/close within this editor session, so
// repeated manual-size generation doesn't reset to hardcoded defaults
// every time the dialog is reopened.
constexpr double DEFAULT_MANUAL_W = 1024.0, DEFAULT_MANUAL_L = 1024.0, DEFAULT_MANUAL_H = 64.0;
static double s_manual_w = DEFAULT_MANUAL_W, s_manual_l = DEFAULT_MANUAL_L, s_manual_h = DEFAULT_MANUAL_H;
auto *manual_w_spin = new SpinBox( 64, 131072, s_manual_w, 0, 64 );
auto *manual_l_spin = new SpinBox( 64, 131072, s_manual_l, 0, 64 );
auto *manual_h_spin = new SpinBox( 64, 131072, s_manual_h, 0, 64 );
form->addRow( "Width (X):", manual_w_spin );
form->addRow( "Length (Y):", manual_l_spin );
form->addRow( "Height (Z):", manual_h_spin );
// Sub-square size: preset combo + Advanced checkbox in one row.
// This and most settings below are remembered across dialog open/close,
// same as Manual Size, since none are specific to one target mode.
static int s_sq_index = 3; // 64
static bool s_advanced = false;
auto *sq_widget = new QWidget;
auto *sq_hbox = new QHBoxLayout( sq_widget );
sq_hbox->setContentsMargins( 0, 0, 0, 0 );
auto *sq_combo = new ComboBox;
for ( int v : { 8, 16, 32, 64, 128, 256, 512, 1024 } )
sq_combo->addItem( QString::number( v ), v );
sq_combo->setCurrentIndex( s_sq_index );
auto *sq_advanced = new QCheckBox( "Advanced" );
sq_advanced->setChecked( s_advanced );
sq_hbox->addWidget( sq_combo );
sq_hbox->addWidget( sq_advanced );
form->addRow( "Sub-square Size:", sq_widget );
// Advanced step X/Y (hidden until Advanced is checked)
static int s_step_x = 64, s_step_y = 64;
auto *step_x_spin = new SpinBox( 8, 512, s_step_x, 0, 8 );
auto *step_y_spin = new SpinBox( 8, 512, s_step_y, 0, 8 );
form->addRow( "Step X:", step_x_spin );
form->addRow( "Step Y:", step_y_spin );
// Jitters each vertex's X/Y position (standard terrain only) so
// triangles vary in size instead of tiling identical rectangles.
static bool s_jitter = false;
auto *jitter_check = new QCheckBox( "Irregular Grid" );
jitter_check->setChecked( s_jitter );
jitter_check->setToolTip( "Varies triangle sizes instead of a uniform grid.\n"
"Occasionally produces an oddly steep or spiky triangle —\n"
"if that happens, just regenerate." );
form->addRow( "", jitter_check );
// Base shape
static int s_shape_index = 0; // Flat
auto *shape_combo = new ComboBox;
shape_combo->addItem( "Flat (None)", (int)ShapeType::Flat );
shape_combo->addItem( "Hill", (int)ShapeType::Hill );
shape_combo->addItem( "Crater", (int)ShapeType::Crater );
shape_combo->addItem( "Ridge", (int)ShapeType::Ridge );
shape_combo->addItem( "Slope", (int)ShapeType::Slope );
shape_combo->addItem( "Volcano", (int)ShapeType::Volcano );
shape_combo->addItem( "Valley", (int)ShapeType::Valley );
shape_combo->addItem( "Tunnel", (int)ShapeType::Tunnel );
shape_combo->addItem( "Slope Tunnel", (int)ShapeType::SlopeTunnel );
shape_combo->setCurrentIndex( s_shape_index );
form->addRow( "Base Shape:", shape_combo );
// Direction for Slope/Ridge/Valley and tunnels; ignored by radially
// symmetric shapes.
static int s_axis_index = (int)Axis::X;
auto *axis_combo = new ComboBox;
axis_combo->addItem( "X Axis", (int)Axis::X );
axis_combo->addItem( "Y Axis", (int)Axis::Y );
axis_combo->setCurrentIndex( s_axis_index );
form->addRow( "Axis:", axis_combo );
// Shape height (relabeled per shape below). Auto-derived from the
// selection for Slope/Slope Tunnel in Use Selection mode (see
// slope_derived below) — the remembered value only shows otherwise.
static double s_shape_height = 256.0;
auto *shape_height_spin = new DoubleSpinBox( 0, 4096, s_shape_height, 2, 8 );
form->addRow( "Peak Height:", shape_height_spin );
// Tunnel height — only visible for Slope Tunnel
static double s_tunnel_height = 256.0;
auto *tunnel_height_spin = new DoubleSpinBox( 0, 4096, s_tunnel_height, 2, 8 );
form->addRow( "Tunnel Height:", tunnel_height_spin );
// Terrace step — hidden for Flat
static double s_terrace = 0.0;
auto *terrace_spin = new DoubleSpinBox( 0, 512, s_terrace, 2, 8 );
form->addRow( "Terrace Step:", terrace_spin );
// Every generated vertex height snaps to a multiple of this, so it lands
// cleanly on the editor's grid for manual editing later.
static int s_grid_step_index = 0; // 1
auto *grid_step_combo = new ComboBox;
for ( int v : { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 } )
grid_step_combo->addItem( QString::number( v ), v );
grid_step_combo->setCurrentIndex( s_grid_step_index );
form->addRow( "Grid Step:", grid_step_combo );
// Noise type
static int s_noise_index = 0; // Perlin
auto *noise_combo = new ComboBox;
noise_combo->addItem( "Perlin Noise", (int)NoiseType::Perlin );
noise_combo->addItem( "Simplex Noise", (int)NoiseType::Simplex );
noise_combo->addItem( "Regular (Random)", (int)NoiseType::Random );
noise_combo->setCurrentIndex( s_noise_index );
form->addRow( "Noise Type:", noise_combo );
// Variance / Frequency
static double s_variance = 32.0, s_frequency = 0.005;
auto *variance_spin = new DoubleSpinBox( 0, 1024, s_variance, 2, 1 );
form->addRow( "Variance:", variance_spin );
auto *frequency_spin = new DoubleSpinBox( 0.0001, 1.0, s_frequency, 4, 0.001 );
form->addRow( "Frequency:", frequency_spin );
// Texture — line edit + Pick button to grab from texture browser.
// Prefers the live browser selection, then the remembered texture,
// then this hardcoded default.
const QString DEFAULT_TEXTURE = "textures/";
static std::string s_texture;
auto *tex_widget = new QWidget;
auto *tex_hbox = new QHBoxLayout( tex_widget );
tex_hbox->setContentsMargins( 0, 0, 0, 0 );
const QString browser_shader = GlobalRadiant().TextureBrowser_getSelectedShader();
const QString initial_texture = !browser_shader.isEmpty() ? browser_shader
: !s_texture.empty() ? QString::fromStdString( s_texture )
: DEFAULT_TEXTURE;
auto *texture_edit = new QLineEdit( initial_texture );
auto *tex_pick = new QPushButton( "Pick" );
tex_pick->setFixedWidth( 48 );
tex_hbox->addWidget( texture_edit );
tex_hbox->addWidget( tex_pick );
// Poll the selection bounds every 250ms so the W/L/H labels stay
// current if the user changes their selection while the dialog is open.
auto *sel_timer = new QTimer( &dialog );
sel_timer->setInterval( 250 );
QObject::connect( sel_timer, &QTimer::timeout, [&](){
if ( target_combo->currentIndex() == 0 )
refresh_sel_labels();
} );
sel_timer->start();
// Poll the texture browser every 100ms so picking a shader in it
// updates the texture field automatically.
QString last_polled_shader = texture_edit->text();
auto *pick_timer = new QTimer( &dialog );
pick_timer->setInterval( 100 );
QObject::connect( pick_timer, &QTimer::timeout, [&](){
const QString current = GlobalRadiant().TextureBrowser_getSelectedShader();
if ( current != last_polled_shader ) {
last_polled_shader = current;
texture_edit->setText( current );
}
} );
pick_timer->start();
// Leave the browser open after picking (auto-closing it was fragile/poor UX).
tex_pick->setToolTip( "Open the texture browser to pick a shader.\n"
"If the browser is docked in the main window layout,\n"
"select a texture there and it fills in here automatically." );
QObject::connect( tex_pick, &QPushButton::clicked, [&](){
GlobalRadiant().TextureBrowser_show();
} );
form->addRow( "Texture:", tex_widget );
// Resets every remembered setting to its original default. Always
// visible (not mode-gated). Target mode is deliberately left alone.
auto *reset_btn = new QPushButton( "Reset to Default" );
QObject::connect( reset_btn, &QPushButton::clicked, [&](){
manual_w_spin->setValue( DEFAULT_MANUAL_W );
manual_l_spin->setValue( DEFAULT_MANUAL_L );
manual_h_spin->setValue( DEFAULT_MANUAL_H );
sq_combo->setCurrentIndex( 3 ); // 64
sq_advanced->setChecked( false );
step_x_spin->setValue( 64 );
step_y_spin->setValue( 64 );
jitter_check->setChecked( false );
shape_combo->setCurrentIndex( 0 ); // Flat
axis_combo->setCurrentIndex( (int)Axis::X );
shape_height_spin->setValue( 256.0 );
tunnel_height_spin->setValue( 256.0 );
terrace_spin->setValue( 0.0 );
grid_step_combo->setCurrentIndex( 0 ); // 1
noise_combo->setCurrentIndex( 0 ); // Perlin
variance_spin->setValue( 32.0 );
frequency_spin->setValue( 0.005 );
texture_edit->setText( DEFAULT_TEXTURE );
} );
form->addRow( "", reset_btn );
// Explicit layout (Generate left, Close right) so order is platform-independent.
auto *btn_widget = new QWidget;
auto *btn_layout = new QHBoxLayout( btn_widget );
btn_layout->setContentsMargins( 0, 12, 0, 0 ); // top spacing from fields
auto *generate_btn = new QPushButton( "Generate" );
auto *close_btn = new QPushButton( "Close" );
btn_layout->addStretch();
btn_layout->addWidget( generate_btn );
btn_layout->addSpacing( 8 );
btn_layout->addWidget( close_btn );
btn_layout->addStretch();
form->addRow( btn_widget );
// Helper: show/hide a form row (widget + its label)
auto set_row_visible = [&]( QWidget *w, bool visible ){
w->setVisible( visible );
if ( auto *lbl = form->labelForField( w ) )
lbl->setVisible( visible );
};
// Generated terrain stores the footprint size it was built from as a key
// on its func_group(s), so regeneration reuses that size (stops growth)
// while taking position from the live selection (follows if moved).
const char* const TERRAINGEN_KEY = "_terraingen_size";
// Fills w/l/h and returns true if any selected node (or its parent entity)
// carries a stored footprint size.
auto read_stored_size = [&]( double& w, double& l, double& h ) -> bool {
struct Reader : public SelectionSystem::Visitor {
const char* key;
double *w, *l, *h;
mutable bool found = false;
Reader( const char* k, double* a, double* b, double* c ) : key( k ), w( a ), l( b ), h( c ){}
void visit( scene::Instance& instance ) const override {
if ( found )
return;
Entity* e = instance_entity( instance );
if ( e == nullptr )
return;
const char* v = e->getKeyValue( key );
if ( v != nullptr && v[0] != '\0'
&& std::sscanf( v, "%lf %lf %lf", w, l, h ) == 3
&& *w >= 1.0 && *l >= 1.0 )
found = true;
}
} reader( TERRAINGEN_KEY, &w, &l, &h );
GlobalSelectionSystem().foreachSelected( reader );
return reader.found;
};
// Writes the footprint size onto the entity of every selected node (the
// freshly generated func_group(s), whose brushes the build step selects).
auto write_stored_size = [&]( const SelBounds& s ){
char buf[128];
std::snprintf( buf, sizeof( buf ), "%g %g %g",
s.x1 - s.x0, s.y1 - s.y0, s.z1 - s.z0 );
struct Writer : public SelectionSystem::Visitor {
const char* key;
const char* value;
Writer( const char* k, const char* v ) : key( k ), value( v ){}
void visit( scene::Instance& instance ) const override {
if ( Entity* e = instance_entity( instance ) )
e->setKeyValue( key, value );
}
} writer( TERRAINGEN_KEY, buf );
GlobalSelectionSystem().foreachSelected( writer );
};
// ----------------------------------------------------------------------
// Shared validation + generation — returns true on success.
// Called by both Generate and OK.
auto do_generate = [&]() -> bool {
if ( target_combo->currentIndex() == 0 ) {
if ( GlobalSelectionSystem().countSelected() == 0 ) {
GlobalRadiant().m_pfnMessageBox( main_window,
"No brush is selected.\n\n"
"Please select a brush in the viewport first,\n"
"or switch the Target to \"Manual Size\".",
"Terrain Generator - No Selection",
EMessageBoxType::Error, 0 );
return false;
}
if ( !query_sel().valid ) {
GlobalRadiant().m_pfnMessageBox( main_window,
"The selected brush is too small.\n\n"
"The terrain area must be at least 64 x 64 units.\n"
"Please select a larger brush,\n"
"or switch the Target to \"Manual Size\".",
"Terrain Generator - Brush Too Small",
EMessageBoxType::Error, 0 );
return false;
}
}
if ( texture_edit->text().trimmed().isEmpty() ) {
GlobalRadiant().m_pfnMessageBox( main_window,
"No texture specified.\n\n"
"Please enter a texture path or use the Pick button\n"
"to select one from the texture browser.",
"Terrain Generator - No Texture",
EMessageBoxType::Error, 0 );
return false;
}
// --- Read parameters ---
const bool use_manual = ( target_combo->currentIndex() == 1 );
const bool advanced = sq_advanced->isChecked();
const double step_x = advanced ? step_x_spin->value() : sq_combo->currentData().toInt();
const double step_y = advanced ? step_y_spin->value() : sq_combo->currentData().toInt();
const ShapeType shape = (ShapeType)shape_combo->currentData().toInt();
const Axis axis = (Axis)axis_combo->currentData().toInt();
const NoiseType noise = (NoiseType)noise_combo->currentData().toInt();
const bool jitter_grid = jitter_check->isChecked();
const double tun_height = tunnel_height_spin->value();
const double variance = variance_spin->value();
const double frequency = frequency_spin->value();
const double terrace = terrace_spin->value();
const double grid_step = grid_step_combo->currentData().toInt();
const std::string texture_str = texture_edit->text().toStdString();
const char* texture = texture_str.c_str();
// --- Determine bounds (must happen before deleting the selection brush) ---
BrushData target;
// Footprint to record on the generated entities, so a later
// regeneration reuses it instead of growing.
SelBounds used_bounds{};
bool used_bounds_valid = false;
if ( use_manual ) {
target = make_manual_brush_data( manual_w_spin->value(), manual_l_spin->value(), manual_h_spin->value() );
}
else {
// Reuse the stored size but recenter on the live selection (peaks/
// walls extend symmetrically, so live center == footprint center).
// A fresh brush has no stored size and uses its own bounds directly.
double sw, sl, sh;
SelBounds s{};
if ( read_stored_size( sw, sl, sh ) ) {
const SelBounds live = query_sel();
if ( live.valid ) {
const double cx = ( live.x0 + live.x1 ) * 0.5;
const double cy = ( live.y0 + live.y1 ) * 0.5;
s.x0 = cx - sw * 0.5; s.x1 = cx + sw * 0.5;
s.y0 = cy - sl * 0.5; s.y1 = cy + sl * 0.5;
s.z0 = live.z0; s.z1 = live.z0 + sh;
s.valid = true;
}
}
else {
s = query_sel( step_x < step_y ? step_x : step_y );
}
if ( s.valid ) {
target.min_x = s.x0;
target.max_x = s.x1;
target.min_y = s.y0;
target.max_y = s.y1;
target.min_z = s.z0;
// Slopes use the slope height (so the low edge stays at z0+64
// instead of sinking below the floor on a steep drop); other
// shapes use the selection height.
const bool is_slope = ( shape == ShapeType::Slope || shape == ShapeType::SlopeTunnel );
const double top_h = is_slope ? std::max( shape_height_spin->value(), 64.0 )
: std::max( s.z1 - s.z0, 64.0 );
target.max_z = s.z0 + top_h;
target.width_x = target.max_x - target.min_x;
target.length_y = target.max_y - target.min_y;
target.height_z = target.max_z - target.min_z;
// Remember the footprint to record on the generated entities
// once the build step completes.
used_bounds = s;
used_bounds_valid = true;
}
else {
target = make_manual_brush_data( manual_w_spin->value(), manual_l_spin->value(), manual_h_spin->value() );
}
}
// For Slope/SlopeTunnel in Use Selection mode, the spinbox holds the
// full brush Z height; negate the drop amount (Z − 64) so the engine
// formula (base_z = shape_height * nx) descends to min_z + 64 instead
// of ascending.
const bool slope_from_sel = !use_manual
&& ( shape == ShapeType::Slope || shape == ShapeType::SlopeTunnel );
const double shape_height = slope_from_sel
? -( shape_height_spin->value() - 64.0 )
: shape_height_spin->value();
// One undo step covering both removing the source brush and creating
// the generated geometry, so a single Undo restores the original
// brush and removes everything we added.
UndoableCommand undo( "terrainGenerator.generate" );
// --- Delete the selection brush now that its bounds are captured ---
if ( !use_manual ) {
while ( GlobalSelectionSystem().countSelected() > 0 ) {
scene::Path path( GlobalSelectionSystem().ultimateSelected().path() );
scene::Path parent_path( path );
parent_path.pop();
Path_deleteTop( path );
// Remove a group entity left empty by the deletion, mirroring
// Radiant's own delete behavior (never the worldspawn or root).
if ( parent_path.size() > 1
&& Node_isEntity( parent_path.top() )
&& !parent_path.top().get().isRoot() ) {
Entity* entity = Node_getEntity( parent_path.top() );
scene::Traversable* children = Node_getTraversable( parent_path.top() );
const bool worldspawn = entity != 0 && string_equal( entity->getClassName(), "worldspawn" );
if ( !worldspawn && children != 0 && children->empty() )
Path_deleteTop( parent_path );
}
}
SceneChangeNotify();
}
adjust_bounds_to_fit_grid( target, step_x, step_y );
// Clear any leftover selection so that only the geometry we are
// about to generate ends up selected (the build step selects it).
GlobalSelectionSystem().setSelectedAll( false );
// --- Generate ---
const bool is_tunnel = ( shape == ShapeType::Tunnel || shape == ShapeType::SlopeTunnel );
globalOutputStream() << "TerrainGenerator: generating "
<< ( is_tunnel ? "tunnel" : "terrain" )
<< " - bounds ("
<< target.width_x << " x " << target.length_y << " x " << target.height_z
<< "), step (" << step_x << " x " << step_y << ")"
<< ", texture: " << texture << "\n";
if ( is_tunnel ) {
const double cave_height = ( shape == ShapeType::SlopeTunnel ) ? tun_height : shape_height;
const double slope_height = ( shape == ShapeType::SlopeTunnel ) ? shape_height : 0;
const double tunnel_terrace = ( shape == ShapeType::SlopeTunnel ) ? terrace : 0.0;
auto maps = generate_tunnel_height_maps( target, step_x, step_y, cave_height, slope_height, variance, frequency, noise, tunnel_terrace, axis, grid_step );
build_tunnel_brushes( target, step_x, step_y, maps, texture, cave_height, slope_height, axis );
}
else {
bool split_diagonally = ( variance > 0 || shape != ShapeType::Flat || jitter_grid );
auto height_map = generate_height_map( target, step_x, step_y, shape, shape_height, variance, frequency, noise, terrace, axis, jitter_grid, grid_step );
build_terrain_brushes( target, step_x, step_y, height_map, texture, split_diagonally );
}
globalOutputStream() << "TerrainGenerator: generation complete\n";
// Record the footprint size on the generated (now selected) entities so
// a future regeneration reuses it instead of the grown geometry's size.
if ( used_bounds_valid )
write_stored_size( used_bounds );
return true;
};
QObject::connect( generate_btn, &QPushButton::clicked, [&](){ do_generate(); } );
QObject::connect( close_btn, &QPushButton::clicked, &dialog, &QDialog::reject );
// Shape height label per shape type (index matches ShapeType enum value)
static const char* shape_height_label[] = {
nullptr, // Flat — row hidden
"Peak Height:", // Hill
"Crater Depth:", // Crater
"Ridge Height:", // Ridge
"Slope Height:", // Slope
"Volcano Height:", // Volcano
"Valley Depth:", // Valley
"Tunnel Height:", // Tunnel
"Slope Height:", // SlopeTunnel
};
// Returns true when slope height should be derived from the selection
// (Use Selection mode + Slope or Slope Tunnel shape).
auto slope_derived = [&]() -> bool {
const ShapeType st = (ShapeType)shape_combo->currentData().toInt();
return target_combo->currentIndex() == 0
&& ( st == ShapeType::Slope || st == ShapeType::SlopeTunnel );
};
// Suppressed only for the initial post-restore call below, so a
// restored height survives dialog open. Only wired into
// update_target_mode — switching shapes never touches this value.
bool auto_derive_slope_height = true;
// Target mode toggle: read-only selection labels vs editable spinboxes.
// Refresh labels each time the user switches to "Use Selection" so they
// reflect whatever is selected at that moment.
auto update_target_mode = [&]( int idx ){
const bool use_sel = ( idx == 0 );
if ( use_sel ) {
refresh_sel_labels();
if ( slope_derived() && auto_derive_slope_height ) {
// Prefer the stored footprint height over the live (grown)
// selection, which would over-steepen the slope.
double sw, sl, sh;
if ( read_stored_size( sw, sl, sh ) ) {
shape_height_spin->setValue( sh );
}
else {
const SelBounds s = query_sel();
if ( s.valid )
shape_height_spin->setValue( s.z1 - s.z0 );
}
}
}
set_row_visible( sel_w_label, use_sel );
set_row_visible( sel_l_label, use_sel );
set_row_visible( sel_h_label, use_sel );
set_row_visible( manual_w_spin, !use_sel );
set_row_visible( manual_l_spin, !use_sel );
set_row_visible( manual_h_spin, !use_sel );
};
// Advanced sub-square toggle
auto update_advanced = [&]( bool advanced ){
sq_combo->setVisible( !advanced );
set_row_visible( step_x_spin, advanced );
set_row_visible( step_y_spin, advanced );
};
// Shape type toggle: labels + visibility only — Axis and shape height
// are fully sticky, never reset by a shape switch.
auto update_shape = [&]( int idx ){
const ShapeType st = (ShapeType)shape_combo->itemData( idx ).toInt();
const bool is_flat = ( st == ShapeType::Flat );
const bool is_slope_tunnel = ( st == ShapeType::SlopeTunnel );
const bool is_tunnel_shape = ( st == ShapeType::Tunnel || st == ShapeType::SlopeTunnel );
const bool has_axis = ( st == ShapeType::Slope || st == ShapeType::Ridge || st == ShapeType::Valley
|| is_tunnel_shape );
set_row_visible( axis_combo, has_axis );
set_row_visible( shape_height_spin, !is_flat );
if ( !is_flat ) {
if ( auto *lbl = qobject_cast<QLabel*>( form->labelForField( shape_height_spin ) ) )
lbl->setText( shape_height_label[idx] );
}
set_row_visible( tunnel_height_spin, is_slope_tunnel );
// Terrace not applicable to flat tunnels (no slope to step),
// but valid for slope tunnels where the floor descends along Y
set_row_visible( terrace_spin, !is_flat && st != ShapeType::Tunnel );
// Irregular grid only applies to standard terrain (generate_height_map),
// not tunnels.
set_row_visible( jitter_check, !is_tunnel_shape );
};
// Wire signals
QObject::connect( target_combo, QOverload<int>::of( &QComboBox::currentIndexChanged ), update_target_mode );
QObject::connect( sq_advanced, &QCheckBox::toggled, update_advanced );
QObject::connect( shape_combo, QOverload<int>::of( &QComboBox::currentIndexChanged ), update_shape );
// Set initial visibility. Slope-height auto-derive is suppressed across
// this call so the just-restored height survives dialog open.
auto_derive_slope_height = false;
update_target_mode( target_combo->currentIndex() );
update_advanced( sq_advanced->isChecked() );
update_shape( shape_combo->currentIndex() );
auto_derive_slope_height = true;
// show() instead of exec() so the dialog is non-modal — the texture
// browser panel (and all other Radiant windows) remain interactive
dialog.show();
dialog.raise();
{
QEventLoop loop;
QObject::connect( &dialog, &QDialog::finished, &loop, &QEventLoop::quit );
loop.exec();
}
s_target_index = target_combo->currentIndex();
s_manual_w = manual_w_spin->value();
s_manual_l = manual_l_spin->value();
s_manual_h = manual_h_spin->value();
s_sq_index = sq_combo->currentIndex();
s_advanced = sq_advanced->isChecked();
s_step_x = step_x_spin->value();
s_step_y = step_y_spin->value();
s_jitter = jitter_check->isChecked();
s_shape_index = shape_combo->currentIndex();
s_axis_index = axis_combo->currentIndex();
s_shape_height = shape_height_spin->value();
s_tunnel_height = tunnel_height_spin->value();
s_terrace = terrace_spin->value();
s_grid_step_index = grid_step_combo->currentIndex();
s_noise_index = noise_combo->currentIndex();
s_variance = variance_spin->value();
s_frequency = frequency_spin->value();
s_texture = texture_edit->text().toStdString();
s_active_dialog = nullptr;
}
}
} // namespace terrain_generator
class TerrainGeneratorDependencies :
public GlobalRadiantModuleRef,
public GlobalUndoModuleRef,
public GlobalSceneGraphModuleRef,
public GlobalSelectionModuleRef,
public GlobalEntityModuleRef,
public GlobalEntityClassManagerModuleRef,
public GlobalBrushModuleRef
{
public:
TerrainGeneratorDependencies() :
GlobalEntityModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entities" ) ),
GlobalEntityClassManagerModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entityclass" ) ),
GlobalBrushModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "brushtypes" ) ){
}
};
class TerrainGeneratorModule : public TypeSystemRef
{
_QERPluginTable m_plugin;
public:
typedef _QERPluginTable Type;
STRING_CONSTANT( Name, "TerrainGenerator" );
TerrainGeneratorModule(){
m_plugin.m_pfnQERPlug_Init = &terrain_generator::init;
m_plugin.m_pfnQERPlug_GetName = &terrain_generator::getName;
m_plugin.m_pfnQERPlug_GetCommandList = &terrain_generator::getCommandList;
m_plugin.m_pfnQERPlug_GetCommandTitleList = &terrain_generator::getCommandTitleList;
m_plugin.m_pfnQERPlug_Dispatch = &terrain_generator::dispatch;
}
_QERPluginTable* getTable(){
return &m_plugin;
}
};
typedef SingletonModule<TerrainGeneratorModule, TerrainGeneratorDependencies> SingletonTerrainGeneratorModule;
SingletonTerrainGeneratorModule g_TerrainGeneratorModule;
extern "C" void RADIANT_DLLEXPORT Radiant_RegisterModules( ModuleServer& server ){
initialiseModule( server );
g_TerrainGeneratorModule.selfRegister();
}