use crate::ecs::{EditorWorld, PlacementPhase, PlacementShape};
use crate::systems::input;
use crate::systems::retained_ui::UiHandles;
use nightshade::ecs::input::resources::MouseState;
use nightshade::ecs::material::components::{AlphaMode, Material};
use nightshade::ecs::world::VISIBILITY;
use nightshade::prelude::*;
const DRAG_THRESHOLD_PIXELS: f32 = 4.0;
#[derive(Clone, Copy)]
pub enum GeneratorKind {
Building,
Room,
Tower,
Stairs,
Columns,
Perimeter,
CityBlock,
Courtyard,
Wfc,
WfcCity,
}
const GENERATORS: &[(GeneratorKind, &str)] = &[
(GeneratorKind::Building, "Building"),
(GeneratorKind::Room, "Room"),
(GeneratorKind::Tower, "Tower"),
(GeneratorKind::Stairs, "Stairs"),
(GeneratorKind::Columns, "Columns"),
(GeneratorKind::Perimeter, "Walls"),
(GeneratorKind::CityBlock, "City block"),
(GeneratorKind::Courtyard, "Courtyard"),
(GeneratorKind::Wfc, "WFC level"),
(GeneratorKind::WfcCity, "WFC city"),
];
#[derive(Default, Clone, Copy)]
pub struct GenerationHandles {
pub footprint_x: Entity,
pub footprint_z: Entity,
pub stories: Entity,
pub story_height: Entity,
pub wall_thickness: Entity,
pub window_spacing: Entity,
pub seed: Entity,
pub variation: Entity,
}
const SHAPES: &[(PlacementShape, &str)] = &[
(PlacementShape::Cube, "Cube"),
(PlacementShape::Plane, "Plane"),
(PlacementShape::Cylinder, "Cylinder"),
(PlacementShape::Cone, "Cone"),
(PlacementShape::Sphere, "Sphere"),
(PlacementShape::Torus, "Torus"),
];
const BLOCK_PRESETS: &[(&str, PlacementShape, [f32; 3])] = &[
("Floor 4x4", PlacementShape::Cube, [4.0, 0.2, 4.0]),
("Floor 2x2", PlacementShape::Cube, [2.0, 0.2, 2.0]),
("Wall 4x3", PlacementShape::Cube, [4.0, 3.0, 0.3]),
("Half wall", PlacementShape::Cube, [4.0, 1.5, 0.3]),
("Pillar", PlacementShape::Cube, [0.6, 3.0, 0.6]),
("Beam", PlacementShape::Cube, [4.0, 0.4, 0.4]),
("Cube 1m", PlacementShape::Cube, [1.0, 1.0, 1.0]),
("Platform", PlacementShape::Cube, [4.0, 0.4, 4.0]),
("Step", PlacementShape::Cube, [2.0, 0.4, 1.0]),
("Crate", PlacementShape::Cube, [1.0, 1.0, 1.0]),
("Column", PlacementShape::Cylinder, [0.6, 3.0, 0.6]),
("Cone marker", PlacementShape::Cone, [1.0, 1.5, 1.0]),
];
#[derive(Default, Clone)]
pub struct CreateShapeHandles {
pub place_button: Entity,
pub push_pull_button: Entity,
pub play_button: Entity,
pub spawn_button: Entity,
pub status_label: Entity,
pub shape_buttons: Vec<(Entity, PlacementShape)>,
pub size_x: Entity,
pub size_y: Entity,
pub size_z: Entity,
pub grid_size: Entity,
pub grid_presets: Vec<(Entity, f32)>,
pub orient_checkbox: Entity,
pub preset_buttons: Vec<(Entity, usize)>,
pub material_swatches: Vec<(Entity, usize)>,
pub generation: GenerationHandles,
pub generate_buttons: Vec<(Entity, GeneratorKind)>,
pub apply_material_button: Entity,
pub snap_grid_button: Entity,
pub mark_brush_button: Entity,
pub upgrade_button: Entity,
pub show_final_button: Entity,
}
pub fn build_in(tree: &mut UiTreeBuilder, content: Entity) -> CreateShapeHandles {
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size;
let mut place_button = Entity::default();
let mut push_pull_button = Entity::default();
let mut play_button = Entity::default();
let mut spawn_button = Entity::default();
let mut status_label = Entity::default();
let mut shape_buttons: Vec<(Entity, PlacementShape)> = Vec::with_capacity(SHAPES.len());
let mut size_x = Entity::default();
let mut size_y = Entity::default();
let mut size_z = Entity::default();
let mut grid_size = Entity::default();
let mut grid_presets: Vec<(Entity, f32)> = Vec::new();
let mut orient_checkbox = Entity::default();
let mut preset_buttons: Vec<(Entity, usize)> = Vec::with_capacity(BLOCK_PRESETS.len());
let mut material_swatches: Vec<(Entity, usize)> =
Vec::with_capacity(super::build::palette_count());
let mut generation = GenerationHandles::default();
let mut generate_buttons: Vec<(Entity, GeneratorKind)> = Vec::with_capacity(GENERATORS.len());
let mut apply_material_button = Entity::default();
let mut snap_grid_button = Entity::default();
let mut mark_brush_button = Entity::default();
let mut upgrade_button = Entity::default();
let mut show_final_button = Entity::default();
let scroll = tree.in_parent(content, |tree| tree.add_scroll_area_fill(4.0, 6.0));
let panel_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
.map(|data| data.content_entity)
.unwrap_or(scroll);
tree.in_parent(panel_root, |tree| {
place_button = tree.add_button("Start placing");
push_pull_button = tree.add_button("Push/Pull faces");
spawn_button = tree.add_button("Add player spawn");
play_button = tree.add_button("Play (first person)");
status_label = tree
.add_node()
.size(100.pct(), (18.0).px())
.with_text("Click to stamp, or shift+drag to draw a block", font * 0.8)
.text_left()
.color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
.entity();
section_label(tree, "Shape", font);
let shape_row = tree
.add_node()
.fill_width()
.auto_size(AutoSizeMode::Height)
.flow(FlowDirection::Horizontal, 0.0, 4.0)
.flow_wrap()
.entity();
tree.in_parent(shape_row, |tree| {
for (shape, label) in SHAPES {
let tooltip = format!("Draw blocks as {label}");
let button = compact_button(tree, label, &tooltip, 64.0, font);
shape_buttons.push((button, *shape));
}
});
let grid = tree.add_property_grid(120.0);
let section = tree.add_property_section(grid, "Size (meters)");
size_x = size_drag(tree, grid, section, "X");
size_y = size_drag(tree, grid, section, "Y");
size_z = size_drag(tree, grid, section, "Z");
let grid_section = tree.add_property_section(grid, "Grid");
let grid_row = tree.add_property_row(grid, grid_section, "Snap");
tree.in_parent(grid_row, |tree| {
grid_size = tree.add_drag_value_configured(
DragValueConfig::new(0.01, f32::MAX, 1.0)
.speed(0.05)
.precision(2),
);
});
let preset_row = tree
.add_node()
.size(100.pct(), (24.0).px())
.flow(FlowDirection::Horizontal, 0.0, 4.0)
.entity();
tree.in_parent(preset_row, |tree| {
for value in [0.25_f32, 0.5, 1.0, 2.0] {
let label = format!("{value}m");
let tooltip = format!("Snap to a {value} meter grid");
let button = compact_button(tree, &label, &tooltip, 44.0, font);
grid_presets.push((button, value));
}
});
orient_checkbox = tree.add_checkbox("Orient to surface", false);
let library_header = tree.add_collapsing_header("Block library", false);
let list_root = widget::<UiCollapsingHeaderData>(tree.world_mut(), library_header)
.map(|data| data.content_entity)
.unwrap_or(library_header);
tree.in_parent(list_root, |tree| {
for (index, (label, _, size)) in BLOCK_PRESETS.iter().enumerate() {
let card = tree
.add_node()
.size(100.pct(), (38.0).px())
.with_rect(4.0, 1.0, vec4(0.0, 0.0, 0.0, 0.0))
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 6.0, 8.0)
.entity();
tree.in_parent(card, |tree| {
let canvas = tree.add_canvas(vec2(30.0, 30.0));
draw_iso_box(tree.world_mut(), canvas, *size, 30.0);
tree.add_node()
.flex_grow(1.0)
.size((0.0).px(), (30.0).px())
.with_text(label, font * 0.85)
.text_left()
.fg(ThemeColor::Text)
.entity();
});
preset_buttons.push((card, index));
}
});
section_label(tree, "Materials", font);
let mat_row = tree
.add_node()
.fill_width()
.auto_size(AutoSizeMode::Height)
.flow(FlowDirection::Horizontal, 0.0, 4.0)
.flow_wrap()
.entity();
tree.in_parent(mat_row, |tree| {
for index in 0..super::build::palette_count() {
let color = super::build::palette_color(index);
let chip = tree
.add_node()
.size((26.0).px(), (26.0).px())
.with_rect(4.0, 1.0, vec4(0.0, 0.0, 0.0, 0.0))
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_interaction()
.with_tooltip(super::build::palette_label(index))
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 3.0, 0.0)
.entity();
tree.in_parent(chip, |tree| {
tree.add_node()
.fill()
.with_rect(3.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0))
.color_raw::<UiBase>(vec4(color[0], color[1], color[2], 1.0))
.entity();
});
material_swatches.push((chip, index));
}
});
section_label(tree, "Generate", font);
let gen_row = tree
.add_node()
.fill_width()
.auto_size(AutoSizeMode::Height)
.flow(FlowDirection::Horizontal, 0.0, 4.0)
.flow_wrap()
.entity();
tree.in_parent(gen_row, |tree| {
for (kind, label) in GENERATORS {
let tooltip = format!("Generate a {} at the view focus", label.to_lowercase());
let button = compact_button(tree, label, &tooltip, 64.0, font);
generate_buttons.push((button, *kind));
}
});
let settings_header = tree.add_collapsing_header("Generation settings", false);
let settings_content = widget::<UiCollapsingHeaderData>(tree.world_mut(), settings_header)
.map(|data| data.content_entity)
.unwrap_or(settings_header);
tree.in_parent(settings_content, |tree| {
let grid = tree.add_property_grid(120.0);
let section = tree.add_property_section(grid, "Building");
generation.footprint_x = settings_drag(tree, grid, section, "Width", 0.5, 0.1, 1);
generation.footprint_z = settings_drag(tree, grid, section, "Depth", 0.5, 0.1, 1);
generation.stories = settings_drag(tree, grid, section, "Stories", 1.0, 0.1, 0);
generation.story_height = settings_drag(tree, grid, section, "Story h", 2.0, 0.1, 1);
generation.wall_thickness = settings_drag(tree, grid, section, "Wall", 0.1, 0.02, 2);
generation.window_spacing = settings_drag(tree, grid, section, "Win gap", 2.0, 0.1, 1);
generation.seed = settings_drag(tree, grid, section, "Seed", 1.0, 1.0, 0);
generation.variation = settings_drag(tree, grid, section, "Variation", 0.0, 0.02, 2);
});
let advanced = tree.add_collapsing_header("Advanced", false);
let advanced_content = widget::<UiCollapsingHeaderData>(tree.world_mut(), advanced)
.map(|data| data.content_entity)
.unwrap_or(advanced);
tree.in_parent(advanced_content, |tree| {
apply_material_button = tree.add_button("Paint selection");
snap_grid_button = tree.add_button("Snap selection to grid");
mark_brush_button = tree.add_button("Mark as brush");
upgrade_button = tree.add_button("Upgrade with glTF...");
show_final_button = tree.add_button("Toggle final / greybox");
});
});
CreateShapeHandles {
place_button,
push_pull_button,
play_button,
spawn_button,
status_label,
shape_buttons,
size_x,
size_y,
size_z,
grid_size,
grid_presets,
orient_checkbox,
preset_buttons,
material_swatches,
generation,
generate_buttons,
apply_material_button,
snap_grid_button,
mark_brush_button,
upgrade_button,
show_final_button,
}
}
fn draw_iso_box(world: &mut World, canvas: Entity, size: [f32; 3], dimension: f32) {
let max_dim = size[0].max(size[1]).max(size[2]).max(0.001);
let scale = (dimension * 0.25) / max_dim;
let half_x = size[0] * 0.5 * scale;
let height = size[1] * scale;
let half_z = size[2] * 0.5 * scale;
let center_x = dimension * 0.5;
let center_y = dimension * 0.66;
let project = |x: f32, y: f32, z: f32| -> Vec2 {
Vec2::new(center_x + (x - z), center_y + (x + z) * 0.5 - y)
};
let corners = [
project(-half_x, 0.0, -half_z),
project(half_x, 0.0, -half_z),
project(half_x, 0.0, half_z),
project(-half_x, 0.0, half_z),
project(-half_x, height, -half_z),
project(half_x, height, -half_z),
project(half_x, height, half_z),
project(-half_x, height, half_z),
];
let edges = [
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
];
let color = vec4(0.45, 0.78, 1.0, 0.95);
ui_canvas_clear(world, canvas);
for (start, end) in edges {
ui_canvas_line(world, canvas, corners[start], corners[end], 1.0, color);
}
}
fn compact_button(
tree: &mut UiTreeBuilder,
label: &str,
tooltip: &str,
width: f32,
font: f32,
) -> Entity {
let button = tree
.add_node()
.size((width).px(), (24.0).px())
.with_rect(4.0, 1.0, vec4(0.0, 0.0, 0.0, 0.0))
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_tooltip(tooltip)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.entity();
tree.in_parent(button, |tree| {
tree.add_node()
.fill()
.with_text(label, font * 0.8)
.text_center()
.fg(ThemeColor::Text)
.entity();
});
button
}
fn section_label(tree: &mut UiTreeBuilder, text: &str, font: f32) {
tree.add_node()
.size(100.pct(), (18.0).px())
.with_text(text, font * 0.85)
.text_left()
.color_raw::<UiBase>(vec4(0.8, 0.85, 1.0, 1.0))
.entity();
}
fn settings_drag(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
min: f32,
speed: f32,
precision: usize,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_drag_value_configured(
DragValueConfig::new(min, f32::MAX, min)
.speed(speed)
.precision(precision),
);
});
widget
}
fn size_drag(tree: &mut UiTreeBuilder, grid: Entity, section: Entity, label: &str) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_drag_value_configured(
DragValueConfig::new(0.01, f32::MAX, 1.0)
.speed(0.1)
.precision(2),
);
});
widget
}
pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
let handles = handles.create_shape.clone();
let mut toggle_active = false;
let mut start_placing = false;
let mut generate: Option<GeneratorKind> = None;
let mut toggle_push_pull = false;
let mut toggle_play = false;
let mut add_spawn = false;
for event in ui_events(world) {
match event {
UiEvent::ButtonClicked(entity) => {
if *entity == handles.place_button {
toggle_active = true;
}
if *entity == handles.push_pull_button {
toggle_push_pull = true;
}
if *entity == handles.play_button {
toggle_play = true;
}
if *entity == handles.spawn_button {
add_spawn = true;
}
for (button, kind) in &handles.generate_buttons {
if *button == *entity {
generate = Some(*kind);
}
}
for (button, shape) in &handles.shape_buttons {
if *button == *entity {
editor_world.resources.placement.shape = *shape;
}
}
for (button, value) in &handles.grid_presets {
if *button == *entity {
editor_world.resources.snap.translation_step = *value;
}
}
for (button, index) in &handles.preset_buttons {
if *button == *entity {
let (_, shape, size) = BLOCK_PRESETS[*index];
editor_world.resources.placement.shape = shape;
editor_world.resources.placement.size =
Vec3::new(size[0], size[1], size[2]);
start_placing = true;
}
}
for (button, index) in &handles.material_swatches {
if *button == *entity {
super::build::set_active_palette(editor_world, *index);
editor_world
.resources
.ui_interaction
.actions
.push(super::Action::ApplyGreyboxToSelection);
}
}
let actions = &mut editor_world.resources.ui_interaction.actions;
if *entity == handles.apply_material_button {
actions.push(super::Action::ApplyGreyboxToSelection);
} else if *entity == handles.snap_grid_button {
actions.push(super::Action::SnapSelectionToGrid);
} else if *entity == handles.mark_brush_button {
actions.push(super::Action::MarkSelectionAsBrush);
} else if *entity == handles.upgrade_button {
actions.push(super::Action::UpgradeSelectionWithGltf);
} else if *entity == handles.show_final_button {
actions.push(super::Action::ToggleShowFinal);
}
}
UiEvent::DragValueChanged { entity, value } => {
let value = *value;
let entity = *entity;
let generation = handles.generation;
if entity == handles.grid_size {
editor_world.resources.snap.translation_step = value.max(0.01);
} else if entity == generation.footprint_x {
editor_world.resources.generation.footprint_x = value.max(0.5);
} else if entity == generation.footprint_z {
editor_world.resources.generation.footprint_z = value.max(0.5);
} else if entity == generation.stories {
editor_world.resources.generation.stories = value.round().max(1.0) as u32;
} else if entity == generation.story_height {
editor_world.resources.generation.story_height = value.max(2.0);
} else if entity == generation.wall_thickness {
editor_world.resources.generation.wall_thickness = value.max(0.05);
} else if entity == generation.window_spacing {
editor_world.resources.generation.window_spacing = value.max(1.0);
} else if entity == generation.seed {
editor_world.resources.generation.seed = value.round().max(1.0) as u32;
} else if entity == generation.variation {
editor_world.resources.generation.variation = value.clamp(0.0, 1.0);
} else if entity == handles.size_x {
editor_world.resources.placement.size.x = value.max(0.01);
} else if entity == handles.size_y {
editor_world.resources.placement.size.y = value.max(0.01);
} else if entity == handles.size_z {
editor_world.resources.placement.size.z = value.max(0.01);
}
}
UiEvent::CheckboxChanged { entity, value } if *entity == handles.orient_checkbox => {
editor_world.resources.placement.orient_to_normal = *value;
}
_ => {}
}
}
if toggle_active {
let active = !editor_world.resources.placement.active;
set_active(editor_world, world, active);
}
if start_placing {
set_active(editor_world, world, true);
}
if toggle_push_pull {
let active = !editor_world.resources.push_pull.active;
if active {
set_active(editor_world, world, false);
}
editor_world.resources.push_pull.active = active;
let button = editor_world
.resources
.ui_handles
.create_shape
.push_pull_button;
ui_button_set_text(
world,
button,
if active {
"Stop push/pull"
} else {
"Push/Pull faces"
},
);
}
if let Some(kind) = generate {
run_generator(editor_world, world, kind);
}
if add_spawn {
crate::systems::play::add_player_spawn(editor_world, world);
}
if toggle_play {
crate::systems::play::toggle_and_sync_button(editor_world, world);
}
update_status(editor_world, world);
}
fn run_generator(editor_world: &mut EditorWorld, world: &mut World, kind: GeneratorKind) {
use super::build::{
BuildCommand, BuildingParams, CityBlockParams, ColumnsParams, CourtyardParams,
PerimeterParams, StairsParams, TowerParams, WfcCityParams, WfcParams,
};
let focus = crate::systems::camera::focus_point(editor_world, world);
let step = super::build::snap_step(editor_world);
let proto = super::build::PROTOTYPE_MATERIAL_COUNT;
let wall_material = editor_world.resources.build.active_palette % proto;
let floor_material = (wall_material + 1) % proto;
let stair_material = (wall_material + 3) % proto;
let settings = &editor_world.resources.generation;
let footprint = Vec2::new(settings.footprint_x.max(2.0), settings.footprint_z.max(2.0));
let stories = settings.stories.max(1);
let story_height = settings.story_height.max(2.0);
let wall_thickness = settings.wall_thickness.max(0.1);
let floor_thickness = settings.floor_thickness.max(0.1);
let window_spacing = settings.window_spacing.max(2.0);
let seed = settings.seed.max(1);
let variation = settings.variation;
let anchor = |footprint: Vec2| {
super::build::snap_translation(
Vec3::new(
focus.x - footprint.x * 0.5,
0.0,
focus.z - footprint.y * 0.5,
),
step,
)
};
let building = |footprint: Vec2, stories: u32| BuildingParams {
min_corner: anchor(footprint),
footprint,
stories,
story_height,
wall_thickness,
floor_thickness,
window_spacing,
floor_material,
wall_material,
stair_material,
};
let command = match kind {
GeneratorKind::Building => BuildCommand::Building(building(footprint, stories)),
GeneratorKind::Room => BuildCommand::Building(building(footprint, 1)),
GeneratorKind::Tower => BuildCommand::Tower(TowerParams {
min_corner: anchor(footprint),
footprint,
stories: stories.max(3),
story_height,
shrink_per_story: 0.4,
floor_thickness,
floor_material,
wall_material,
}),
GeneratorKind::Stairs => {
let stair_footprint = Vec2::new(footprint.x.min(4.0), footprint.y);
BuildCommand::Stairs(StairsParams {
min_corner: anchor(stair_footprint),
width: stair_footprint.x,
steps: (story_height / 0.25).round().max(4.0) as u32,
rise: 0.25,
run: 0.35,
material: stair_material,
})
}
GeneratorKind::Columns => BuildCommand::Columns(ColumnsParams {
min_corner: anchor(footprint),
footprint,
spacing: window_spacing,
column: 0.6,
height: story_height,
floor_thickness,
floor_material,
column_material: wall_material,
}),
GeneratorKind::Perimeter => BuildCommand::Perimeter(PerimeterParams {
min_corner: anchor(footprint),
footprint,
height: story_height,
thickness: wall_thickness,
gate_width: 3.0,
material: wall_material,
}),
GeneratorKind::CityBlock => {
let street = step.max(2.0);
let span = Vec2::new(
footprint.x * 3.0 + street * 2.0,
footprint.y * 3.0 + street * 2.0,
);
BuildCommand::CityBlock(CityBlockParams {
min_corner: anchor(span),
columns: 3,
rows: 3,
lot: footprint,
street,
max_stories: stories.max(2),
story_height,
wall_thickness,
floor_thickness,
window_spacing,
seed,
variation,
floor_material,
wall_material,
})
}
GeneratorKind::Courtyard => BuildCommand::Courtyard(CourtyardParams {
min_corner: anchor(footprint),
footprint,
wing_depth: (footprint.x.min(footprint.y) * 0.3).max(3.0),
stories,
story_height,
wall_thickness,
floor_thickness,
window_spacing,
floor_material,
wall_material,
}),
GeneratorKind::Wfc => {
let cell = window_spacing.max(3.0);
let span = Vec2::new(cell * 6.0, cell * 6.0);
BuildCommand::Wfc(WfcParams {
min_corner: anchor(span),
columns: 6,
rows: 6,
cell,
wall_height: story_height,
wall_thickness,
floor_thickness,
seed,
floor_material,
wall_material,
})
}
GeneratorKind::WfcCity => {
let cell = footprint.x.max(8.0) + 2.0;
let span = Vec2::new(cell * 10.0, cell * 10.0);
let count = super::build::palette_count();
BuildCommand::WfcCity(WfcCityParams {
min_corner: anchor(span),
columns: 10,
rows: 10,
cell,
road_width: 2.0,
max_stories: stories.max(2) + 4,
story_height,
wall_thickness,
floor_thickness,
window_spacing,
seed,
road_material: 1 % count,
park_material: 2 % count,
floor_material: 6 % count,
wall_material,
})
}
};
super::build::apply_command(editor_world, world, command);
}
fn sync_drag(world: &mut World, entity: Entity, value: f32) {
if let Some(data) = world.ui.get_ui_drag_value_mut(entity)
&& !data.editing
&& (data.value - value).abs() > f32::EPSILON
{
data.value = value;
}
}
pub fn sync(editor_world: &EditorWorld, world: &mut World) {
let handles = &editor_world.resources.ui_handles.create_shape;
sync_drag(
world,
handles.grid_size,
editor_world.resources.snap.translation_step,
);
let settings = &editor_world.resources.generation;
let generation = handles.generation;
sync_drag(world, generation.footprint_x, settings.footprint_x);
sync_drag(world, generation.footprint_z, settings.footprint_z);
sync_drag(world, generation.stories, settings.stories as f32);
sync_drag(world, generation.story_height, settings.story_height);
sync_drag(world, generation.wall_thickness, settings.wall_thickness);
sync_drag(world, generation.window_spacing, settings.window_spacing);
sync_drag(world, generation.seed, settings.seed as f32);
sync_drag(world, generation.variation, settings.variation);
let (accent, surface) = {
let theme = world.resources.retained_ui.theme_state.active_theme();
(theme.accent_color, theme.background_color)
};
let step = editor_world.resources.snap.translation_step;
let active_shape = editor_world.resources.placement.shape;
let active_palette = editor_world.resources.build.active_palette;
for (button, shape) in &handles.shape_buttons {
let color = if *shape == active_shape {
accent
} else {
surface
};
set_node_fill(world, *button, color);
}
for (card, index) in &handles.material_swatches {
let color = if *index == active_palette {
accent
} else {
surface
};
set_node_fill(world, *card, color);
}
for (button, value) in &handles.grid_presets {
let color = if (*value - step).abs() < 1.0e-4 {
accent
} else {
surface
};
set_node_fill(world, *button, color);
}
let placing = editor_world.resources.placement.active;
set_node_fill(
world,
handles.place_button,
if placing { accent } else { surface },
);
set_node_fill(
world,
handles.push_pull_button,
if editor_world.resources.push_pull.active {
accent
} else {
surface
},
);
}
fn set_node_fill(world: &mut World, entity: Entity, color: Vec4) {
if let Some(node_color) = world.ui.get_ui_node_color_mut(entity) {
node_color.colors[0] = Some(color);
}
}
pub fn open(editor_world: &mut EditorWorld, world: &mut World) {
let tab_bar = editor_world.resources.ui_handles.tree.tab_bar;
ui_tab_bar_set_value(world, tab_bar, 1);
}
pub fn stop(editor_world: &mut EditorWorld, world: &mut World) {
if editor_world.resources.placement.active {
set_active(editor_world, world, false);
}
}
fn set_active(editor_world: &mut EditorWorld, world: &mut World, active: bool) {
editor_world.resources.placement.active = active;
editor_world.resources.placement.phase = PlacementPhase::Hover;
editor_world.resources.placement.press_screen = None;
if active {
editor_world.resources.snap.enabled = true;
editor_world.resources.push_pull.active = false;
let button = editor_world
.resources
.ui_handles
.create_shape
.push_pull_button;
ui_button_set_text(world, button, "Push/Pull faces");
} else {
clear_ghost(editor_world, world);
}
let button = editor_world.resources.ui_handles.create_shape.place_button;
ui_button_set_text(
world,
button,
if active {
"Stop placing"
} else {
"Start placing"
},
);
}
fn update_status(editor_world: &EditorWorld, world: &mut World) {
let label = editor_world.resources.ui_handles.create_shape.status_label;
let text = if editor_world.resources.placement.active {
match editor_world.resources.placement.phase {
PlacementPhase::Hover => "Click to stamp, or shift+drag to draw a footprint",
PlacementPhase::Footprint { .. } => "Drag the footprint, release to set it",
PlacementPhase::Height { .. } => "Move to set height, click to place (Esc cancels)",
}
} else if editor_world.resources.push_pull.active {
"Drag a block face along its normal to resize (Esc cancels)"
} else {
"Pick a shape, then press Start placing"
};
ui_set_text(world, label, text);
}
fn set_status_text(editor_world: &EditorWorld, world: &mut World, text: &str) {
let label = editor_world.resources.ui_handles.create_shape.status_label;
ui_set_text(world, label, text);
}
pub fn tick(editor_world: &mut EditorWorld, world: &mut World) {
if !editor_world.resources.placement.active {
clear_ghost(editor_world, world);
editor_world.resources.placement.phase = PlacementPhase::Hover;
editor_world.resources.placement.press_screen = None;
return;
}
let mouse = *nightshade::ecs::input::access::mouse_for_active(world);
let in_viewport = input::mouse_in_active_viewport(world);
let ui_block = input::ui_capturing(world);
let usable = in_viewport && !ui_block;
let just_pressed = mouse.state.contains(MouseState::LEFT_JUST_PRESSED);
let just_released = mouse.state.contains(MouseState::LEFT_JUST_RELEASED);
let held = mouse.state.contains(MouseState::LEFT_CLICKED);
let shift = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftRight);
let cancel = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::Escape);
let surface = if usable {
surface_under_cursor(editor_world, world, mouse.position)
} else {
None
};
if cancel {
editor_world.resources.placement.phase = PlacementPhase::Hover;
editor_world.resources.placement.press_screen = None;
update_ghost(editor_world, world, None);
return;
}
let step = super::build::snap_step(editor_world);
let size = editor_world.resources.placement.size;
let shape = editor_world.resources.placement.shape;
let orient = editor_world.resources.placement.orient_to_normal;
let mut preview: Option<GhostTransform> = None;
match editor_world.resources.placement.phase {
PlacementPhase::Hover => {
if usable && just_pressed {
editor_world.resources.placement.press_screen = Some(mouse.position);
}
let press = editor_world.resources.placement.press_screen;
let moved = press.is_some_and(|start| {
held && nalgebra_glm::distance(&start, &mouse.position) > DRAG_THRESHOLD_PIXELS
});
if moved
&& shift
&& !orient
&& let Some((hit, _)) = surface
{
let corner = snap_footprint_point(editor_world, world, hit, step);
editor_world.resources.placement.phase = PlacementPhase::Footprint {
start_corner: corner,
};
preview = Some(aabb_to_ghost(shape, corner, corner));
} else if just_released && press.is_some() {
let was_click = press.is_some_and(|start| {
nalgebra_glm::distance(&start, &mouse.position) <= DRAG_THRESHOLD_PIXELS
});
editor_world.resources.placement.press_screen = None;
if was_click && let Some((hit, normal)) = surface {
let ghost = stamp_ghost(shape, hit, normal, size, orient, step);
commit(editor_world, world, ghost);
}
} else if let Some((hit, normal)) = surface {
preview = Some(stamp_ghost(shape, hit, normal, size, orient, step));
}
}
PlacementPhase::Footprint { start_corner } => {
let opposite =
footprint_corner(editor_world, world, mouse.position, start_corner.y, step)
.unwrap_or(start_corner);
let (min, max) = footprint_bounds(start_corner, opposite);
preview = Some(aabb_to_ghost(
shape,
min,
footprint_slab_top(min, max, step),
));
set_status_text(
editor_world,
world,
&format!("{:.2} x {:.2} m", max.x - min.x, max.z - min.z),
);
if just_released {
editor_world.resources.placement.press_screen = None;
let area = (max.x - min.x) * (max.z - min.z);
if area < step * step * 0.25 {
editor_world.resources.placement.phase = PlacementPhase::Hover;
} else {
editor_world.resources.placement.phase = PlacementPhase::Height {
footprint_min: min,
footprint_max: max,
height: step,
};
}
}
}
PlacementPhase::Height {
footprint_min,
footprint_max,
height,
} => {
let height =
height_from_cursor(world, mouse.position, footprint_min, footprint_max, step)
.unwrap_or(height);
let top = Vec3::new(footprint_max.x, footprint_min.y + height, footprint_max.z);
preview = Some(aabb_to_ghost(shape, footprint_min, top));
set_status_text(
editor_world,
world,
&format!(
"{:.2} x {:.2} x {:.2} m",
footprint_max.x - footprint_min.x,
height,
footprint_max.z - footprint_min.z,
),
);
if usable && just_pressed {
editor_world.resources.placement.phase = PlacementPhase::Hover;
editor_world.resources.placement.press_screen = None;
let ghost = aabb_to_ghost(shape, footprint_min, top);
commit(editor_world, world, ghost);
preview = None;
} else {
editor_world.resources.placement.phase = PlacementPhase::Height {
footprint_min,
footprint_max,
height,
};
}
}
}
update_ghost(editor_world, world, preview);
}
fn update_ghost(
editor_world: &mut EditorWorld,
world: &mut World,
preview: Option<GhostTransform>,
) {
let shape = editor_world.resources.placement.shape;
if editor_world.resources.placement.ghost_shape != Some(shape) {
clear_ghost(editor_world, world);
}
let Some(transform) = preview else {
if let Some(ghost) = editor_world.resources.placement.ghost_entity {
set_entity_visible(world, ghost, false);
}
clear_ghost_outline(world);
return;
};
let ghost = match editor_world.resources.placement.ghost_entity {
Some(ghost) => ghost,
None => {
let ghost = spawn_ghost(world, shape);
editor_world.resources.placement.ghost_entity = Some(ghost);
editor_world.resources.placement.ghost_shape = Some(shape);
ghost
}
};
set_entity_visible(world, ghost, true);
if let Some(local) = world.core.get_local_transform_mut(ghost) {
local.translation = transform.translation;
local.scale = transform.scale;
local.rotation = transform.rotation;
}
mark_local_transform_dirty(world, ghost);
world
.resources
.editor_selection
.bounding_volume_selected_entity = None;
world.resources.editor_selection.selected_entities = vec![ghost];
}
fn clear_ghost_outline(world: &mut World) {
world.resources.editor_selection.selected_entities.clear();
}
fn spawn_ghost(world: &mut World, shape: PlacementShape) -> Entity {
let entity = super::build::spawn_shape(world, shape, Vec3::zeros());
world.core.add_components(entity, VISIBILITY);
let name = register_ghost_material(world);
world.core.set_material_ref(entity, MaterialRef::new(name));
entity
}
fn register_ghost_material(world: &mut World) -> String {
let name = "greybox_ghost".to_string();
let material = Material {
base_color: [0.4, 0.75, 1.0, 0.4],
alpha_mode: AlphaMode::Blend,
unlit: true,
..Default::default()
};
material_registry_insert(
&mut world.resources.assets.material_registry,
name.clone(),
material,
);
if let Some(&index) = world
.resources
.assets
.material_registry
.registry
.name_to_index
.get(&name)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
name
}
fn set_entity_visible(world: &mut World, entity: Entity, visible: bool) {
world.core.add_components(entity, VISIBILITY);
world.core.set_visibility(entity, Visibility { visible });
}
fn clear_ghost(editor_world: &mut EditorWorld, world: &mut World) {
if let Some(ghost) = editor_world.resources.placement.ghost_entity.take() {
despawn_recursive_immediate(world, ghost);
}
editor_world.resources.placement.ghost_shape = None;
clear_ghost_outline(world);
}
#[derive(Clone, Copy)]
struct GhostTransform {
translation: Vec3,
scale: Vec3,
rotation: Quat,
}
fn aabb_to_ghost(shape: PlacementShape, min: Vec3, max: Vec3) -> GhostTransform {
let block = super::build::block_from_aabb(shape, min, max, 0);
GhostTransform {
translation: block.translation,
scale: block.scale,
rotation: block.rotation,
}
}
fn stamp_ghost(
shape: PlacementShape,
hit: Vec3,
normal: Vec3,
size: Vec3,
orient: bool,
step: f32,
) -> GhostTransform {
if orient {
let full = super::build::shape_full_extent(shape);
let half_y = if full.y > 1.0e-6 { size.y * 0.5 } else { 0.0 };
return GhostTransform {
translation: hit + nalgebra_glm::normalize(&normal) * half_y,
scale: Vec3::new(
super::build::axis_scale(size.x, full.x),
super::build::axis_scale(size.y, full.y),
super::build::axis_scale(size.z, full.z),
),
rotation: rotation_y_to_normal(normal),
};
}
let (min, max) = stamp_aabb(hit, normal, size, step);
aabb_to_ghost(shape, min, max)
}
fn stamp_aabb(hit: Vec3, normal: Vec3, size: Vec3, step: f32) -> (Vec3, Vec3) {
let normal = if normal.magnitude() < 1.0e-6 {
Vec3::new(0.0, 1.0, 0.0)
} else {
nalgebra_glm::normalize(&normal)
};
let dominant = if normal.x.abs() >= normal.y.abs() && normal.x.abs() >= normal.z.abs() {
0
} else if normal.y.abs() >= normal.z.abs() {
1
} else {
2
};
let mut min = Vec3::zeros();
let mut max = Vec3::zeros();
for axis in 0..3 {
if axis == dominant {
if normal[axis] >= 0.0 {
min[axis] = hit[axis];
max[axis] = hit[axis] + size[axis];
} else {
max[axis] = hit[axis];
min[axis] = hit[axis] - size[axis];
}
} else {
let low = snap_to_grid_scalar(hit[axis] - size[axis] * 0.5, step);
min[axis] = low;
max[axis] = low + size[axis];
}
}
(min, max)
}
fn snap_to_grid_scalar(value: f32, step: f32) -> f32 {
if step <= 0.0 {
value
} else {
(value / step).round() * step
}
}
fn snap_point_xz(point: Vec3, step: f32) -> Vec3 {
Vec3::new(
snap_to_grid_scalar(point.x, step),
point.y,
snap_to_grid_scalar(point.z, step),
)
}
fn snap_footprint_point(editor_world: &EditorWorld, world: &World, point: Vec3, step: f32) -> Vec3 {
let ghost = editor_world.resources.placement.ghost_entity;
let threshold = (step * 0.5).max(0.25);
let mut best: Option<Vec3> = None;
let mut best_distance = threshold;
for entity in world
.core
.query_entities(nightshade::ecs::world::BOUNDING_VOLUME)
{
if Some(entity) == ghost || editor_world.resources.editor_scene.is_scaffolding(entity) {
continue;
}
let (Some(bounding_volume), Some(global)) = (
world.core.get_bounding_volume(entity),
world.core.get_global_transform(entity),
) else {
continue;
};
for corner in bounding_volume.transform(&global.0).obb.get_corners() {
let distance = ((corner.x - point.x).powi(2) + (corner.z - point.z).powi(2)).sqrt();
if distance < best_distance {
best_distance = distance;
best = Some(Vec3::new(corner.x, point.y, corner.z));
}
}
}
best.unwrap_or_else(|| snap_point_xz(point, step))
}
fn footprint_corner(
editor_world: &EditorWorld,
world: &World,
mouse: Vec2,
base_y: f32,
step: f32,
) -> Option<Vec3> {
let point = get_ground_position_from_screen(world, mouse, base_y)?;
Some(snap_footprint_point(
editor_world,
world,
Vec3::new(point.x, base_y, point.z),
step,
))
}
fn footprint_bounds(a: Vec3, b: Vec3) -> (Vec3, Vec3) {
let min = Vec3::new(a.x.min(b.x), a.y, a.z.min(b.z));
let max = Vec3::new(a.x.max(b.x), a.y, a.z.max(b.z));
(min, max)
}
fn footprint_slab_top(min: Vec3, max: Vec3, step: f32) -> Vec3 {
Vec3::new(max.x, min.y + step.max(0.05), max.z)
}
fn height_from_cursor(world: &World, mouse: Vec2, min: Vec3, max: Vec3, step: f32) -> Option<f32> {
let center = Vec3::new((min.x + max.x) * 0.5, min.y, (min.z + max.z) * 0.5);
let camera_entity = world.resources.active_camera?;
let camera_transform = world.core.get_global_transform(camera_entity)?;
let forward = camera_transform.forward_vector();
let mut normal = Vec3::new(forward.x, 0.0, forward.z);
if normal.magnitude() < 1.0e-3 {
normal = Vec3::new(0.0, 0.0, 1.0);
}
let normal = nalgebra_glm::normalize(&normal);
let ray = PickingRay::from_screen_position(world, mouse)?;
let plane_distance = -nalgebra_glm::dot(&normal, ¢er);
let hit = ray.intersect_plane(normal, plane_distance)?;
let raw = hit.y - min.y;
Some(snap_to_grid_scalar(raw, step).max(step))
}
fn surface_under_cursor(
editor_world: &EditorWorld,
world: &World,
mouse_position: Vec2,
) -> Option<(Vec3, Vec3)> {
let ghost = editor_world.resources.placement.ghost_entity;
let hit = pick_entities(world, mouse_position, PickingOptions::default())
.into_iter()
.find(|result| {
Some(result.entity) != ghost
&& !editor_world
.resources
.editor_scene
.is_scaffolding(result.entity)
});
if let Some(hit) = hit {
let normal = obb_face_normal(world, hit.entity, hit.world_position);
return Some((hit.world_position, normal));
}
let ground = get_ground_position_from_screen(world, mouse_position, 0.0)?;
Some((ground, Vec3::new(0.0, 1.0, 0.0)))
}
fn obb_face_normal(world: &World, entity: Entity, point: Vec3) -> Vec3 {
let up = Vec3::new(0.0, 1.0, 0.0);
let Some(bounding_volume) = world.core.get_bounding_volume(entity) else {
return up;
};
let Some(global) = world.core.get_global_transform(entity) else {
return up;
};
let obb = bounding_volume.transform(&global.0).obb;
let inverse_orientation = obb.orientation.conjugate();
let local = nalgebra_glm::quat_rotate_vec3(&inverse_orientation, &(point - obb.center));
let ratio = Vec3::new(
local.x / obb.half_extents.x.max(1.0e-6),
local.y / obb.half_extents.y.max(1.0e-6),
local.z / obb.half_extents.z.max(1.0e-6),
);
let local_normal = if ratio.x.abs() >= ratio.y.abs() && ratio.x.abs() >= ratio.z.abs() {
Vec3::new(ratio.x.signum(), 0.0, 0.0)
} else if ratio.y.abs() >= ratio.z.abs() {
Vec3::new(0.0, ratio.y.signum(), 0.0)
} else {
Vec3::new(0.0, 0.0, ratio.z.signum())
};
nalgebra_glm::quat_rotate_vec3(&obb.orientation, &local_normal)
}
fn rotation_y_to_normal(normal: Vec3) -> Quat {
let up = Vec3::new(0.0, 1.0, 0.0);
let normal = nalgebra_glm::normalize(&normal);
let dot = nalgebra_glm::dot(&up, &normal);
if dot > 0.9999 {
return Quat::identity();
}
if dot < -0.9999 {
return nalgebra_glm::quat_angle_axis(std::f32::consts::PI, &Vec3::new(1.0, 0.0, 0.0));
}
let axis = nalgebra_glm::normalize(&nalgebra_glm::cross(&up, &normal));
nalgebra_glm::quat_angle_axis(dot.acos(), &axis)
}
fn commit(editor_world: &mut EditorWorld, world: &mut World, transform: GhostTransform) {
let block = super::build::PlacedBlock {
shape: editor_world.resources.placement.shape,
translation: transform.translation,
scale: transform.scale,
rotation: transform.rotation,
material: editor_world.resources.build.active_palette,
visible: true,
};
super::build::apply_command(
editor_world,
world,
super::build::BuildCommand::PlaceBlocks(vec![block]),
);
}