use crate::ecs::{EditorWorld, PlacementShape};
use nightshade::ecs::loading::load_texture_pack_from_image_bytes;
use nightshade::ecs::material::components::{Material, TextureTransform};
use nightshade::ecs::physics::components::{ColliderComponent, RigidBodyComponent};
use nightshade::prelude::*;
use nightshade::render::wgpu::texture_cache::{SamplerSettings, TextureUsage};
const PROTOTYPE_TEXTURES: &[(&str, &[u8])] = &[
(
"proto_light",
include_bytes!("../../../../../assets/textures/prototype/light/texture_01.png"),
),
(
"proto_dark",
include_bytes!("../../../../../assets/textures/prototype/dark/texture_01.png"),
),
(
"proto_green",
include_bytes!("../../../../../assets/textures/prototype/green/texture_05.png"),
),
(
"proto_orange",
include_bytes!("../../../../../assets/textures/prototype/orange/texture_05.png"),
),
(
"proto_purple",
include_bytes!("../../../../../assets/textures/prototype/purple/texture_08.png"),
),
(
"proto_red",
include_bytes!("../../../../../assets/textures/prototype/red/texture_05.png"),
),
];
#[derive(Clone, Copy)]
enum Swatch {
Prototype {
key: &'static str,
},
Color {
rgb: [f32; 3],
roughness: f32,
metallic: f32,
},
Emissive {
rgb: [f32; 3],
strength: f32,
},
}
struct PaletteEntry {
label: &'static str,
material_name: &'static str,
swatch: Swatch,
}
const PROTOTYPE_TILING: f32 = 2.0;
const PALETTE: &[PaletteEntry] = &[
PaletteEntry {
label: "Proto Light",
material_name: "greybox_proto_light",
swatch: Swatch::Prototype { key: "proto_light" },
},
PaletteEntry {
label: "Proto Dark",
material_name: "greybox_proto_dark",
swatch: Swatch::Prototype { key: "proto_dark" },
},
PaletteEntry {
label: "Proto Green",
material_name: "greybox_proto_green",
swatch: Swatch::Prototype { key: "proto_green" },
},
PaletteEntry {
label: "Proto Orange",
material_name: "greybox_proto_orange",
swatch: Swatch::Prototype {
key: "proto_orange",
},
},
PaletteEntry {
label: "Proto Purple",
material_name: "greybox_proto_purple",
swatch: Swatch::Prototype {
key: "proto_purple",
},
},
PaletteEntry {
label: "Proto Red",
material_name: "greybox_proto_red",
swatch: Swatch::Prototype { key: "proto_red" },
},
PaletteEntry {
label: "Concrete",
material_name: "greybox_concrete",
swatch: Swatch::Color {
rgb: [0.58, 0.58, 0.60],
roughness: 0.9,
metallic: 0.0,
},
},
PaletteEntry {
label: "Stone",
material_name: "greybox_stone",
swatch: Swatch::Color {
rgb: [0.50, 0.47, 0.42],
roughness: 0.85,
metallic: 0.0,
},
},
PaletteEntry {
label: "Wood",
material_name: "greybox_wood",
swatch: Swatch::Color {
rgb: [0.55, 0.40, 0.24],
roughness: 0.7,
metallic: 0.0,
},
},
PaletteEntry {
label: "Metal",
material_name: "greybox_metal",
swatch: Swatch::Color {
rgb: [0.60, 0.62, 0.66],
roughness: 0.35,
metallic: 0.85,
},
},
PaletteEntry {
label: "White",
material_name: "greybox_white",
swatch: Swatch::Color {
rgb: [0.90, 0.90, 0.90],
roughness: 0.8,
metallic: 0.0,
},
},
PaletteEntry {
label: "Emissive Cyan",
material_name: "greybox_emissive_cyan",
swatch: Swatch::Emissive {
rgb: [0.12, 0.80, 1.0],
strength: 5.0,
},
},
PaletteEntry {
label: "Emissive Orange",
material_name: "greybox_emissive_orange",
swatch: Swatch::Emissive {
rgb: [1.0, 0.52, 0.16],
strength: 5.0,
},
},
];
pub fn palette_count() -> usize {
PALETTE.len()
}
pub const PROTOTYPE_MATERIAL_COUNT: usize = 6;
pub fn palette_label(index: usize) -> &'static str {
PALETTE[index % PALETTE.len()].label
}
pub fn palette_color(index: usize) -> [f32; 3] {
match PALETTE[index % PALETTE.len()].swatch {
Swatch::Prototype { .. } => [0.72, 0.72, 0.74],
Swatch::Color { rgb, .. } => rgb,
Swatch::Emissive { rgb, .. } => rgb,
}
}
pub fn set_active_palette(editor_world: &mut EditorWorld, index: usize) {
editor_world.resources.build.active_palette = index % PALETTE.len();
}
pub fn load_palette_textures(editor_world: &mut EditorWorld, world: &mut World) {
if editor_world.resources.build.textures_loaded {
return;
}
load_texture_pack_from_image_bytes(
world,
PROTOTYPE_TEXTURES,
TextureUsage::Color,
SamplerSettings::DEFAULT,
);
editor_world.resources.build.textures_loaded = true;
}
pub fn snap_step(editor_world: &EditorWorld) -> f32 {
editor_world.resources.snap.translation_step.max(0.0001)
}
const GRID_HALF_CELLS: i32 = 40;
const GRID_COLOR: nightshade::prelude::Vec4 =
nightshade::prelude::Vec4::new(0.32, 0.36, 0.44, 0.45);
const GRID_AXIS_COLOR: nightshade::prelude::Vec4 =
nightshade::prelude::Vec4::new(0.55, 0.60, 0.72, 0.8);
pub fn update_grid(editor_world: &mut EditorWorld, world: &mut World) {
if !editor_world.resources.placement.active && !editor_world.resources.push_pull.active {
if let Some(entity) = editor_world.resources.build.grid_lines {
world.core.set_lines(entity, Lines::default());
}
return;
}
let step = snap_step(editor_world);
let entity = ensure_grid_entity(editor_world, world);
world
.core
.set_lines(entity, Lines::new(build_grid_lines(step)));
}
fn ensure_grid_entity(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
use nightshade::ecs::world::{GLOBAL_TRANSFORM, LINES, VISIBILITY};
if let Some(entity) = editor_world.resources.build.grid_lines
&& world.core.entity_has_components(entity, LINES)
{
return entity;
}
let entity = spawn_entities(world, LINES | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
world.core.set_lines(entity, Lines::default());
world
.core
.set_visibility(entity, Visibility { visible: true });
world
.core
.set_global_transform(entity, GlobalTransform::default());
editor_world
.resources
.editor_scene
.register_scaffolding(entity);
editor_world.resources.build.grid_lines = Some(entity);
entity
}
fn build_grid_lines(step: f32) -> Vec<Line> {
let step = step.max(0.01);
let extent = step * GRID_HALF_CELLS as f32;
let mut lines = Vec::with_capacity((GRID_HALF_CELLS as usize * 2 + 1) * 2);
for index in -GRID_HALF_CELLS..=GRID_HALF_CELLS {
let offset = index as f32 * step;
let color = if index == 0 {
GRID_AXIS_COLOR
} else {
GRID_COLOR
};
lines.push(Line {
start: Vec3::new(offset, 0.0, -extent),
end: Vec3::new(offset, 0.0, extent),
color,
});
lines.push(Line {
start: Vec3::new(-extent, 0.0, offset),
end: Vec3::new(extent, 0.0, offset),
color,
});
}
lines
}
pub fn snap_translation(position: Vec3, step: f32) -> Vec3 {
if step <= 0.0 {
return position;
}
Vec3::new(
(position.x / step).round() * step,
(position.y / step).round() * step,
(position.z / step).round() * step,
)
}
#[derive(Clone, Copy)]
pub struct PlacedBlock {
pub shape: PlacementShape,
pub translation: Vec3,
pub scale: Vec3,
pub rotation: Quat,
pub material: usize,
pub visible: bool,
}
pub fn shape_full_extent(shape: PlacementShape) -> Vec3 {
match shape {
PlacementShape::Cube => Vec3::new(1.0, 1.0, 1.0),
PlacementShape::Plane => Vec3::new(2.0, 0.0, 2.0),
PlacementShape::Cylinder => Vec3::new(1.0, 1.0, 1.0),
PlacementShape::Cone => Vec3::new(1.0, 1.0, 1.0),
PlacementShape::Sphere => Vec3::new(2.0, 2.0, 2.0),
PlacementShape::Torus => Vec3::new(2.6, 0.6, 2.6),
}
}
pub fn axis_scale(size: f32, full: f32) -> f32 {
if full > 1.0e-6 { size / full } else { 1.0 }
}
pub fn block_from_aabb(
shape: PlacementShape,
min: Vec3,
max: Vec3,
material: usize,
) -> PlacedBlock {
let full = shape_full_extent(shape);
let size = max - min;
let axis_center = |min_value: f32, max_value: f32, full_value: f32| {
if full_value > 1.0e-6 {
(min_value + max_value) * 0.5
} else {
min_value
}
};
PlacedBlock {
shape,
translation: Vec3::new(
axis_center(min.x, max.x, full.x),
axis_center(min.y, max.y, full.y),
axis_center(min.z, max.z, full.z),
),
scale: Vec3::new(
axis_scale(size.x, full.x),
axis_scale(size.y, full.y),
axis_scale(size.z, full.z),
),
rotation: Quat::identity(),
material,
visible: true,
}
}
pub fn spawn_shape(world: &mut World, shape: PlacementShape, position: Vec3) -> Entity {
match shape {
PlacementShape::Cube => spawn_cube_at(world, position),
PlacementShape::Plane => spawn_plane_at(world, position),
PlacementShape::Cylinder => spawn_cylinder_at(world, position),
PlacementShape::Cone => spawn_cone_at(world, position),
PlacementShape::Sphere => spawn_sphere_at(world, position),
PlacementShape::Torus => spawn_torus_at(world, position),
}
}
pub fn spawn_block(world: &mut World, block: &PlacedBlock) -> Entity {
use nightshade::ecs::world::VISIBILITY;
let entity = spawn_shape(world, block.shape, block.translation);
if let Some(local) = world.core.get_local_transform_mut(entity) {
local.translation = block.translation;
local.scale = block.scale;
local.rotation = block.rotation;
}
mark_local_transform_dirty(world, entity);
apply_to_entity(world, block.material, entity);
world.core.add_components(entity, VISIBILITY);
world.core.set_visibility(
entity,
Visibility {
visible: block.visible,
},
);
add_block_collider(world, entity, block);
entity
}
fn add_block_collider(world: &mut World, entity: Entity, block: &PlacedBlock) {
use nightshade::ecs::world::{COLLIDER, RIGID_BODY};
let full = shape_full_extent(block.shape);
let half = Vec3::new(
(block.scale.x * full.x * 0.5).max(0.05),
(block.scale.y * full.y * 0.5).max(0.05),
(block.scale.z * full.z * 0.5).max(0.05),
);
world.core.add_components(entity, RIGID_BODY | COLLIDER);
if let Some(rigid_body) = world.core.get_rigid_body_mut(entity) {
*rigid_body = RigidBodyComponent::new_static().with_translation(
block.translation.x,
block.translation.y,
block.translation.z,
);
}
if let Some(collider) = world.core.get_collider_mut(entity) {
*collider = ColliderComponent::new_cuboid(half.x, half.y, half.z).with_friction(0.9);
}
}
fn spawn_group(world: &mut World, name: &str) -> Entity {
use nightshade::ecs::world::{GLOBAL_TRANSFORM, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, NAME};
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
1,
)[0];
world.core.set_name(
entity,
nightshade::ecs::world::components::Name(name.to_string()),
);
mark_local_transform_dirty(world, entity);
entity
}
pub fn place_blocks(
editor_world: &mut EditorWorld,
world: &mut World,
blocks: &[PlacedBlock],
label: &str,
) {
let mut entities = Vec::new();
for block in blocks {
if block.scale.x.abs() < 1.0e-4
|| block.scale.y.abs() < 1.0e-4
|| block.scale.z.abs() < 1.0e-4
{
continue;
}
entities.push(spawn_block(world, block));
}
if entities.is_empty() {
return;
}
let root = if entities.len() > 1 {
let group = spawn_group(world, label);
for &entity in &entities {
update_parent(
world,
entity,
Some(nightshade::ecs::transform::components::Parent(Some(group))),
);
}
group
} else {
entities[0]
};
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
root,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
label,
);
crate::systems::selection::set_primary(editor_world, Some(root));
}
const DOOR_WIDTH: f32 = 1.6;
const DOOR_HEIGHT: f32 = 2.4;
const WINDOW_WIDTH: f32 = 1.6;
const WINDOW_SILL: f32 = 1.1;
const WINDOW_HEIGHT: f32 = 1.4;
#[derive(Clone, Copy)]
struct Opening {
width: f32,
bottom: f32,
height: f32,
}
fn box_block(min: Vec3, max: Vec3, material: usize) -> PlacedBlock {
block_from_aabb(PlacementShape::Cube, min, max, material)
}
fn push_wall(
blocks: &mut Vec<PlacedBlock>,
min: Vec3,
max: Vec3,
opening: Option<Opening>,
material: usize,
) {
let Some(opening) = opening else {
blocks.push(box_block(min, max, material));
return;
};
let length_axis = if (max.x - min.x) >= (max.z - min.z) {
0
} else {
2
};
let low = min[length_axis];
let high = max[length_axis];
let center = (low + high) * 0.5;
let half = (opening.width * 0.5)
.min((high - low) * 0.5 - 0.05)
.max(0.0);
let open_low = center - half;
let open_high = center + half;
let open_bottom = (min.y + opening.bottom).clamp(min.y, max.y);
let open_top = (min.y + opening.bottom + opening.height).clamp(min.y, max.y);
if open_low > low + 1.0e-3 {
let mut jamb_max = max;
jamb_max[length_axis] = open_low;
blocks.push(box_block(min, jamb_max, material));
}
if high > open_high + 1.0e-3 {
let mut jamb_min = min;
jamb_min[length_axis] = open_high;
blocks.push(box_block(jamb_min, max, material));
}
if open_bottom > min.y + 1.0e-3 {
let mut sill_min = min;
let mut sill_max = max;
sill_min[length_axis] = open_low;
sill_max[length_axis] = open_high;
sill_max.y = open_bottom;
blocks.push(box_block(sill_min, sill_max, material));
}
if open_top < max.y - 1.0e-3 {
let mut lintel_min = min;
let mut lintel_max = max;
lintel_min[length_axis] = open_low;
lintel_max[length_axis] = open_high;
lintel_min.y = open_top;
blocks.push(box_block(lintel_min, lintel_max, material));
}
}
fn push_wall_bays(
blocks: &mut Vec<PlacedBlock>,
min: Vec3,
max: Vec3,
opening: Opening,
spacing: f32,
material: usize,
) {
let length_axis = if (max.x - min.x) >= (max.z - min.z) {
0
} else {
2
};
let length = max[length_axis] - min[length_axis];
let bays = ((length / spacing.max(0.5)).floor() as u32).max(1);
let bay = length / bays as f32;
for index in 0..bays {
let mut bay_min = min;
let mut bay_max = max;
bay_min[length_axis] = min[length_axis] + index as f32 * bay;
bay_max[length_axis] = bay_min[length_axis] + bay;
push_wall(blocks, bay_min, bay_max, Some(opening), material);
}
}
fn rng_step(state: &mut u32) -> u32 {
let mut value = *state;
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
*state = value.max(1);
*state
}
fn rng_unit(state: &mut u32) -> f32 {
(rng_step(state) & 0x00ff_ffff) as f32 / 0x0100_0000 as f32
}
fn rng_range(state: &mut u32, low: f32, high: f32) -> f32 {
low + rng_unit(state) * (high - low)
}
#[derive(Clone, Copy)]
pub struct BuildingParams {
pub min_corner: Vec3,
pub footprint: Vec2,
pub stories: u32,
pub story_height: f32,
pub wall_thickness: f32,
pub floor_thickness: f32,
pub window_spacing: f32,
pub floor_material: usize,
pub wall_material: usize,
pub stair_material: usize,
}
fn push_floor_with_hole(
blocks: &mut Vec<PlacedBlock>,
min: Vec3,
max: Vec3,
hole_min: Vec3,
hole_max: Vec3,
material: usize,
) {
let top = max.y;
if hole_min.z > min.z + 1.0e-3 {
blocks.push(box_block(min, Vec3::new(max.x, top, hole_min.z), material));
}
if hole_max.z < max.z - 1.0e-3 {
blocks.push(box_block(
Vec3::new(min.x, min.y, hole_max.z),
max,
material,
));
}
if hole_min.x > min.x + 1.0e-3 {
blocks.push(box_block(
Vec3::new(min.x, min.y, hole_min.z),
Vec3::new(hole_min.x, top, hole_max.z),
material,
));
}
if hole_max.x < max.x - 1.0e-3 {
blocks.push(box_block(
Vec3::new(hole_max.x, min.y, hole_min.z),
Vec3::new(max.x, top, hole_max.z),
material,
));
}
}
fn ramp_block(center: Vec3, size: Vec3, rotation: Quat, material: usize) -> PlacedBlock {
PlacedBlock {
shape: PlacementShape::Cube,
translation: center,
scale: size,
rotation,
material,
visible: false,
}
}
fn push_flight(
blocks: &mut Vec<PlacedBlock>,
min: Vec3,
max: Vec3,
ascending_positive_z: bool,
material: usize,
) {
let (x0, x1) = (min.x, max.x);
let (z0, z1) = (min.z, max.z);
let (low_y, high_y) = (min.y, max.y);
let depth = (z1 - z0).max(0.5);
let climb = (high_y - low_y).max(0.1);
let steps = (climb / 0.18).round().max(4.0) as u32;
let rise = climb / steps as f32;
let run = depth / steps as f32;
for step in 0..steps {
let height = low_y + (step as f32 + 1.0) * rise;
let (front, back) = if ascending_positive_z {
(z0 + step as f32 * run, z1)
} else {
(z0, z1 - step as f32 * run)
};
blocks.push(box_block(
Vec3::new(x0, low_y, front),
Vec3::new(x1, height, back),
material,
));
}
let angle = climb.atan2(depth);
let length = (depth * depth + climb * climb).sqrt();
let sign = if ascending_positive_z { -1.0 } else { 1.0 };
let rotation = nalgebra_glm::quat_angle_axis(sign * angle, &Vec3::new(1.0, 0.0, 0.0));
let center = Vec3::new((x0 + x1) * 0.5, low_y + climb * 0.5 - 0.1, (z0 + z1) * 0.5);
blocks.push(ramp_block(
center,
Vec3::new(x1 - x0, 0.3, length),
rotation,
material,
));
}
pub fn generate_building(params: &BuildingParams) -> Vec<PlacedBlock> {
let thickness = params.wall_thickness.max(0.1);
let width = params.footprint.x.max(thickness * 4.0 + DOOR_WIDTH);
let depth = params.footprint.y.max(thickness * 4.0 + DOOR_WIDTH);
let floor_thickness = params.floor_thickness.max(0.1);
let story_height = params.story_height.max(2.4);
let stories = params.stories.max(1);
let x0 = params.min_corner.x;
let z0 = params.min_corner.z;
let x1 = x0 + width;
let z1 = z0 + depth;
let window = Opening {
width: WINDOW_WIDTH,
bottom: WINDOW_SILL,
height: WINDOW_HEIGHT,
};
let door = Opening {
width: DOOR_WIDTH,
bottom: 0.0,
height: DOOR_HEIGHT,
};
let spacing = params.window_spacing.max(2.0);
let climb = story_height + floor_thickness;
let available_depth = (depth - thickness * 2.0 - 0.5).max(1.5);
let stair_depth = (climb * 1.9).clamp(1.5, available_depth);
let available_width = (width - thickness * 2.0 - 0.5).max(1.5);
let stair_width = (width * 0.5).clamp(2.6, 4.6).min(available_width);
let sx0 = x0 + thickness;
let sx1 = (sx0 + stair_width).min(x1 - thickness);
let sxm = (sx0 + sx1) * 0.5;
let sz0 = z0 + thickness;
let sz1 = (sz0 + stair_depth).min(z1 - thickness);
let mut blocks = Vec::with_capacity(stories as usize * 20);
let mut base_y = params.min_corner.y;
for story in 0..stories {
let floor_top = base_y + floor_thickness;
if story == 0 {
blocks.push(box_block(
Vec3::new(x0, base_y, z0),
Vec3::new(x1, floor_top, z1),
params.floor_material,
));
} else {
let hole_on_left = story % 2 == 1;
let (hole_x0, hole_x1) = if hole_on_left { (sx0, sxm) } else { (sxm, sx1) };
push_floor_with_hole(
&mut blocks,
Vec3::new(x0, base_y, z0),
Vec3::new(x1, floor_top, z1),
Vec3::new(hole_x0, base_y, sz0),
Vec3::new(hole_x1, floor_top, sz1),
params.floor_material,
);
}
let wall_top = floor_top + story_height;
let front_min = Vec3::new(x0, floor_top, z0);
let front_max = Vec3::new(x1, wall_top, z0 + thickness);
if story == 0 {
push_wall(
&mut blocks,
front_min,
front_max,
Some(door),
params.wall_material,
);
} else {
push_wall_bays(
&mut blocks,
front_min,
front_max,
window,
spacing,
params.wall_material,
);
}
push_wall_bays(
&mut blocks,
Vec3::new(x0, floor_top, z1 - thickness),
Vec3::new(x1, wall_top, z1),
window,
spacing,
params.wall_material,
);
push_wall_bays(
&mut blocks,
Vec3::new(x0, floor_top, z0 + thickness),
Vec3::new(x0 + thickness, wall_top, z1 - thickness),
window,
spacing,
params.wall_material,
);
push_wall_bays(
&mut blocks,
Vec3::new(x1 - thickness, floor_top, z0 + thickness),
Vec3::new(x1, wall_top, z1 - thickness),
window,
spacing,
params.wall_material,
);
if story + 1 < stories {
let on_left = story % 2 == 0;
let (flight_x0, flight_x1) = if on_left { (sx0, sxm) } else { (sxm, sx1) };
push_flight(
&mut blocks,
Vec3::new(flight_x0, floor_top, sz0),
Vec3::new(flight_x1, floor_top + climb, sz1),
on_left,
params.stair_material,
);
}
base_y = wall_top;
}
blocks.push(box_block(
Vec3::new(x0, base_y, z0),
Vec3::new(x1, base_y + floor_thickness, z1),
params.floor_material,
));
blocks
}
#[derive(Clone, Copy)]
pub struct TowerParams {
pub min_corner: Vec3,
pub footprint: Vec2,
pub stories: u32,
pub story_height: f32,
pub shrink_per_story: f32,
pub floor_thickness: f32,
pub floor_material: usize,
pub wall_material: usize,
}
pub fn generate_tower(params: &TowerParams) -> Vec<PlacedBlock> {
let stories = params.stories.max(1);
let story_height = params.story_height.max(2.0);
let floor_thickness = params.floor_thickness.max(0.1);
let thickness = 0.3_f32;
let mut blocks = Vec::new();
let mut base_y = params.min_corner.y;
let center_x = params.min_corner.x + params.footprint.x * 0.5;
let center_z = params.min_corner.z + params.footprint.y * 0.5;
for story in 0..stories {
let inset = params.shrink_per_story * story as f32;
let half_x = (params.footprint.x * 0.5 - inset).max(1.5);
let half_z = (params.footprint.y * 0.5 - inset).max(1.5);
let x0 = center_x - half_x;
let x1 = center_x + half_x;
let z0 = center_z - half_z;
let z1 = center_z + half_z;
let floor_top = base_y + floor_thickness;
blocks.push(box_block(
Vec3::new(x0, base_y, z0),
Vec3::new(x1, floor_top, z1),
params.floor_material,
));
let wall_top = floor_top + story_height;
let window = Opening {
width: WINDOW_WIDTH,
bottom: WINDOW_SILL,
height: WINDOW_HEIGHT,
};
for (a, b) in [
(
Vec3::new(x0, floor_top, z0),
Vec3::new(x1, wall_top, z0 + thickness),
),
(
Vec3::new(x0, floor_top, z1 - thickness),
Vec3::new(x1, wall_top, z1),
),
(
Vec3::new(x0, floor_top, z0 + thickness),
Vec3::new(x0 + thickness, wall_top, z1 - thickness),
),
(
Vec3::new(x1 - thickness, floor_top, z0 + thickness),
Vec3::new(x1, wall_top, z1 - thickness),
),
] {
push_wall(&mut blocks, a, b, Some(window), params.wall_material);
}
base_y = wall_top;
}
blocks
}
#[derive(Clone, Copy)]
pub struct StairsParams {
pub min_corner: Vec3,
pub width: f32,
pub steps: u32,
pub rise: f32,
pub run: f32,
pub material: usize,
}
pub fn generate_stairs(params: &StairsParams) -> Vec<PlacedBlock> {
let steps = params.steps.max(1);
let rise = params.rise.max(0.05);
let run = params.run.max(0.1);
let width = params.width.max(0.5);
let x0 = params.min_corner.x;
let x1 = x0 + width;
let y0 = params.min_corner.y;
let z0 = params.min_corner.z;
let mut blocks = Vec::with_capacity(steps as usize);
for step in 0..steps {
let step_front = z0 + step as f32 * run;
blocks.push(box_block(
Vec3::new(x0, y0, step_front),
Vec3::new(x1, y0 + (step as f32 + 1.0) * rise, z0 + steps as f32 * run),
params.material,
));
}
blocks
}
#[derive(Clone, Copy)]
pub struct ColumnsParams {
pub min_corner: Vec3,
pub footprint: Vec2,
pub spacing: f32,
pub column: f32,
pub height: f32,
pub floor_thickness: f32,
pub floor_material: usize,
pub column_material: usize,
}
pub fn generate_columns(params: &ColumnsParams) -> Vec<PlacedBlock> {
let spacing = params.spacing.max(1.0);
let column = params.column.max(0.2);
let height = params.height.max(1.0);
let floor_thickness = params.floor_thickness.max(0.1);
let x0 = params.min_corner.x;
let z0 = params.min_corner.z;
let x1 = x0 + params.footprint.x.max(spacing);
let z1 = z0 + params.footprint.y.max(spacing);
let floor_top = params.min_corner.y + floor_thickness;
let mut blocks = vec![box_block(
Vec3::new(x0, params.min_corner.y, z0),
Vec3::new(x1, floor_top, z1),
params.floor_material,
)];
let columns_x = ((x1 - x0) / spacing).floor().max(1.0) as u32;
let columns_z = ((z1 - z0) / spacing).floor().max(1.0) as u32;
let margin = spacing * 0.5;
for ix in 0..=columns_x {
for iz in 0..=columns_z {
let cx = x0 + margin + ix as f32 * spacing;
let cz = z0 + margin + iz as f32 * spacing;
if cx + column * 0.5 > x1 || cz + column * 0.5 > z1 {
continue;
}
blocks.push(box_block(
Vec3::new(cx - column * 0.5, floor_top, cz - column * 0.5),
Vec3::new(cx + column * 0.5, floor_top + height, cz + column * 0.5),
params.column_material,
));
}
}
blocks
}
#[derive(Clone, Copy)]
pub struct PerimeterParams {
pub min_corner: Vec3,
pub footprint: Vec2,
pub height: f32,
pub thickness: f32,
pub gate_width: f32,
pub material: usize,
}
pub fn generate_perimeter(params: &PerimeterParams) -> Vec<PlacedBlock> {
let thickness = params.thickness.max(0.1);
let height = params.height.max(1.0);
let x0 = params.min_corner.x;
let z0 = params.min_corner.z;
let x1 = x0 + params.footprint.x.max(thickness * 4.0 + params.gate_width);
let z1 = z0 + params.footprint.y.max(thickness * 4.0);
let y0 = params.min_corner.y;
let gate = Opening {
width: params.gate_width.max(1.0),
bottom: 0.0,
height: (height - 0.3).max(1.0),
};
let mut blocks = Vec::with_capacity(4);
push_wall(
&mut blocks,
Vec3::new(x0, y0, z0),
Vec3::new(x1, y0 + height, z0 + thickness),
Some(gate),
params.material,
);
push_wall(
&mut blocks,
Vec3::new(x0, y0, z1 - thickness),
Vec3::new(x1, y0 + height, z1),
None,
params.material,
);
push_wall(
&mut blocks,
Vec3::new(x0, y0, z0 + thickness),
Vec3::new(x0 + thickness, y0 + height, z1 - thickness),
None,
params.material,
);
push_wall(
&mut blocks,
Vec3::new(x1 - thickness, y0, z0 + thickness),
Vec3::new(x1, y0 + height, z1 - thickness),
None,
params.material,
);
blocks
}
#[derive(Clone, Copy)]
pub struct CityBlockParams {
pub min_corner: Vec3,
pub columns: u32,
pub rows: u32,
pub lot: Vec2,
pub street: f32,
pub max_stories: u32,
pub story_height: f32,
pub wall_thickness: f32,
pub floor_thickness: f32,
pub window_spacing: f32,
pub seed: u32,
pub variation: f32,
pub floor_material: usize,
pub wall_material: usize,
}
pub fn generate_city_block(params: &CityBlockParams) -> Vec<PlacedBlock> {
let columns = params.columns.max(1);
let rows = params.rows.max(1);
let max_stories = params.max_stories.max(1);
let variation = params.variation.clamp(0.0, 1.0);
let mut state = params.seed.max(1);
let mut blocks = Vec::new();
for row in 0..rows {
for column in 0..columns {
let x = params.min_corner.x + column as f32 * (params.lot.x + params.street);
let z = params.min_corner.z + row as f32 * (params.lot.y + params.street);
let random = 1.0 + rng_unit(&mut state) * (max_stories as f32 - 1.0);
let stories =
(max_stories as f32 * (1.0 - variation) + random * variation).round() as u32;
let inset = rng_range(
&mut state,
0.0,
params.lot.x.min(params.lot.y) * 0.2 * variation,
);
let footprint = Vec2::new(params.lot.x - inset, params.lot.y - inset);
blocks.extend(generate_building(&BuildingParams {
min_corner: Vec3::new(x + inset * 0.5, params.min_corner.y, z + inset * 0.5),
footprint,
stories: stories.max(1),
story_height: params.story_height,
wall_thickness: params.wall_thickness,
floor_thickness: params.floor_thickness,
window_spacing: params.window_spacing,
floor_material: params.floor_material,
wall_material: params.wall_material,
stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
}));
}
}
blocks
}
#[derive(Clone, Copy)]
pub struct CourtyardParams {
pub min_corner: Vec3,
pub footprint: Vec2,
pub wing_depth: f32,
pub stories: u32,
pub story_height: f32,
pub wall_thickness: f32,
pub floor_thickness: f32,
pub window_spacing: f32,
pub floor_material: usize,
pub wall_material: usize,
}
pub fn generate_courtyard(params: &CourtyardParams) -> Vec<PlacedBlock> {
let width = params.footprint.x.max(8.0);
let depth = params.footprint.y.max(8.0);
let wing = params.wing_depth.clamp(2.0, width.min(depth) * 0.5 - 1.0);
let origin = params.min_corner;
let building = |min_corner: Vec3, footprint: Vec2| BuildingParams {
min_corner,
footprint,
stories: params.stories,
story_height: params.story_height,
wall_thickness: params.wall_thickness,
floor_thickness: params.floor_thickness,
window_spacing: params.window_spacing,
floor_material: params.floor_material,
wall_material: params.wall_material,
stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
};
let mut blocks = Vec::new();
blocks.extend(generate_building(&building(origin, Vec2::new(width, wing))));
blocks.extend(generate_building(&building(
Vec3::new(origin.x, origin.y, origin.z + depth - wing),
Vec2::new(width, wing),
)));
let inner_depth = depth - 2.0 * wing;
if inner_depth > 1.0 {
blocks.extend(generate_building(&building(
Vec3::new(origin.x, origin.y, origin.z + wing),
Vec2::new(wing, inner_depth),
)));
blocks.extend(generate_building(&building(
Vec3::new(origin.x + width - wing, origin.y, origin.z + wing),
Vec2::new(wing, inner_depth),
)));
}
blocks
}
#[derive(Clone, Copy)]
pub struct WfcParams {
pub min_corner: Vec3,
pub columns: u32,
pub rows: u32,
pub cell: f32,
pub wall_height: f32,
pub wall_thickness: f32,
pub floor_thickness: f32,
pub seed: u32,
pub floor_material: usize,
pub wall_material: usize,
}
#[derive(Clone, Copy)]
struct WfcTile {
sockets: [u8; 4],
floor: bool,
weight: f32,
}
fn wfc_tiles() -> Vec<WfcTile> {
let mut tiles = Vec::new();
let mut push = |sockets: [u8; 4], floor: bool, weight: f32| {
tiles.push(WfcTile {
sockets,
floor,
weight,
});
};
push([1, 1, 1, 1], true, 0.4);
for sockets in [[1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]] {
push(sockets, true, 0.8);
}
push([1, 0, 1, 0], true, 1.4);
push([0, 1, 0, 1], true, 1.4);
for sockets in [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1]] {
push(sockets, true, 1.2);
}
for sockets in [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] {
push(sockets, true, 0.6);
}
push([0, 0, 0, 0], true, 0.5);
push([0, 0, 0, 0], false, 0.6);
tiles
}
const WFC_OPPOSITE: [usize; 4] = [2, 3, 0, 1];
fn wfc_side_sockets(sockets: &[[u8; 4]], mask: u32, side: usize) -> [bool; 2] {
let mut present = [false; 2];
for (index, tile) in sockets.iter().enumerate() {
if mask & (1 << index) != 0 {
present[tile[side] as usize] = true;
}
}
present
}
fn wfc_solve(
columns: usize,
rows: usize,
sockets: &[[u8; 4]],
weights: &[f32],
seed: u32,
) -> Option<Vec<usize>> {
let count = columns * rows;
let full = if sockets.len() >= 32 {
u32::MAX
} else {
(1u32 << sockets.len()) - 1
};
let mut cells = vec![full; count];
let mut state = seed.max(1);
let allow_side = |cells: &mut [u32], index: usize, side: usize, value: u8| {
let mut mask = 0u32;
for (tile_index, tile) in sockets.iter().enumerate() {
if tile[side] == value {
mask |= 1 << tile_index;
}
}
cells[index] &= mask;
};
for column in 0..columns {
allow_side(&mut cells, column, 0, 0);
allow_side(&mut cells, (rows - 1) * columns + column, 2, 0);
}
for row in 0..rows {
allow_side(&mut cells, row * columns, 3, 0);
allow_side(&mut cells, row * columns + columns - 1, 1, 0);
}
loop {
let mut target = None;
let mut best = u32::MAX;
for (index, &mask) in cells.iter().enumerate() {
let options = mask.count_ones();
if options == 0 {
return None;
}
if options > 1 && options < best {
best = options;
target = Some(index);
}
}
let Some(index) = target else {
break;
};
let mut total = 0.0;
for (tile_index, weight) in weights.iter().enumerate() {
if cells[index] & (1 << tile_index) != 0 {
total += weight;
}
}
let mut pick = rng_unit(&mut state) * total;
let mut chosen = 0;
for (tile_index, weight) in weights.iter().enumerate() {
if cells[index] & (1 << tile_index) != 0 {
pick -= weight;
if pick <= 0.0 {
chosen = tile_index;
break;
}
}
}
cells[index] = 1 << chosen;
let mut stack = vec![index];
while let Some(current) = stack.pop() {
let column = current % columns;
let row = current / columns;
let neighbors = [
(row > 0).then(|| current - columns),
(column + 1 < columns).then_some(current + 1),
(row + 1 < rows).then(|| current + columns),
(column > 0).then(|| current - 1),
];
for (side, neighbor) in neighbors.into_iter().enumerate() {
let Some(neighbor) = neighbor else {
continue;
};
let present = wfc_side_sockets(sockets, cells[current], side);
let mut allowed = 0u32;
for (tile_index, tile) in sockets.iter().enumerate() {
if present[tile[WFC_OPPOSITE[side]] as usize] {
allowed |= 1 << tile_index;
}
}
let next = cells[neighbor] & allowed;
if next != cells[neighbor] {
cells[neighbor] = next;
if next == 0 {
return None;
}
stack.push(neighbor);
}
}
}
}
Some(
cells
.iter()
.map(|mask| mask.trailing_zeros() as usize)
.collect(),
)
}
pub fn generate_wfc(params: &WfcParams) -> Vec<PlacedBlock> {
let columns = params.columns.max(1) as usize;
let rows = params.rows.max(1) as usize;
let tiles = wfc_tiles();
let sockets: Vec<[u8; 4]> = tiles.iter().map(|tile| tile.sockets).collect();
let weights: Vec<f32> = tiles.iter().map(|tile| tile.weight).collect();
let mut grid = None;
for attempt in 0..16 {
if let Some(solution) = wfc_solve(
columns,
rows,
&sockets,
&weights,
params.seed.wrapping_add(attempt),
) {
grid = Some(solution);
break;
}
}
let Some(grid) = grid else {
return Vec::new();
};
let cell = params.cell.max(1.0);
let thickness = params.wall_thickness.max(0.1);
let floor_thickness = params.floor_thickness.max(0.1);
let wall_height = params.wall_height.max(1.0);
let mut blocks = Vec::new();
for row in 0..rows {
for column in 0..columns {
let tile = tiles[grid[row * columns + column]];
if !tile.floor {
continue;
}
let x0 = params.min_corner.x + column as f32 * cell;
let z0 = params.min_corner.z + row as f32 * cell;
let x1 = x0 + cell;
let z1 = z0 + cell;
let base = params.min_corner.y;
let floor_top = base + floor_thickness;
blocks.push(box_block(
Vec3::new(x0, base, z0),
Vec3::new(x1, floor_top, z1),
params.floor_material,
));
let wall_top = floor_top + wall_height;
let mut wall = |min: Vec3, max: Vec3| {
blocks.push(box_block(min, max, params.wall_material));
};
if tile.sockets[0] == 0 {
wall(
Vec3::new(x0, floor_top, z0),
Vec3::new(x1, wall_top, z0 + thickness),
);
}
if tile.sockets[2] == 0 {
wall(
Vec3::new(x0, floor_top, z1 - thickness),
Vec3::new(x1, wall_top, z1),
);
}
if tile.sockets[3] == 0 {
wall(
Vec3::new(x0, floor_top, z0),
Vec3::new(x0 + thickness, wall_top, z1),
);
}
if tile.sockets[1] == 0 {
wall(
Vec3::new(x1 - thickness, floor_top, z0),
Vec3::new(x1, wall_top, z1),
);
}
}
}
blocks
}
#[derive(Clone, Copy, PartialEq)]
enum CityCell {
Road,
Plot,
Park,
}
#[derive(Clone, Copy)]
pub struct WfcCityParams {
pub min_corner: Vec3,
pub columns: u32,
pub rows: u32,
pub cell: f32,
pub road_width: f32,
pub max_stories: u32,
pub story_height: f32,
pub wall_thickness: f32,
pub floor_thickness: f32,
pub window_spacing: f32,
pub seed: u32,
pub road_material: usize,
pub park_material: usize,
pub floor_material: usize,
pub wall_material: usize,
}
fn city_tiles() -> (Vec<[u8; 4]>, Vec<f32>, Vec<CityCell>) {
let mut sockets = Vec::new();
let mut weights = Vec::new();
let mut kinds = Vec::new();
let mut push = |socket: [u8; 4], weight: f32, kind: CityCell| {
sockets.push(socket);
weights.push(weight);
kinds.push(kind);
};
push([1, 1, 1, 1], 0.1, CityCell::Road);
for socket in [[1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]] {
push(socket, 0.2, CityCell::Road);
}
push([1, 0, 1, 0], 0.8, CityCell::Road);
push([0, 1, 0, 1], 0.8, CityCell::Road);
for socket in [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1]] {
push(socket, 0.4, CityCell::Road);
}
push([0, 0, 0, 0], 8.0, CityCell::Plot);
push([0, 0, 0, 0], 1.5, CityCell::Park);
(sockets, weights, kinds)
}
pub fn generate_wfc_city(params: &WfcCityParams) -> Vec<PlacedBlock> {
let columns = params.columns.max(2) as usize;
let rows = params.rows.max(2) as usize;
let (sockets, weights, kinds) = city_tiles();
let mut grid = None;
for attempt in 0..24 {
if let Some(solution) = wfc_solve(
columns,
rows,
&sockets,
&weights,
params.seed.wrapping_add(attempt),
) {
grid = Some(solution);
break;
}
}
let Some(grid) = grid else {
return Vec::new();
};
let cell = params.cell.max(6.0);
let road = params.road_width.clamp(1.0, cell * 0.4);
let floor_thickness = params.floor_thickness.max(0.1);
let mut state = params.seed.max(1);
let mut blocks = Vec::new();
for row in 0..rows {
for column in 0..columns {
let kind = kinds[grid[row * columns + column]];
let x0 = params.min_corner.x + column as f32 * cell;
let z0 = params.min_corner.z + row as f32 * cell;
let base = params.min_corner.y;
let ground_top = base + floor_thickness;
let ground_material = match kind {
CityCell::Road => params.road_material,
CityCell::Park => params.park_material,
CityCell::Plot => params.floor_material,
};
blocks.push(box_block(
Vec3::new(x0, base, z0),
Vec3::new(x0 + cell, ground_top, z0 + cell),
ground_material,
));
if kind == CityCell::Plot {
let footprint = Vec2::new(cell - road * 2.0, cell - road * 2.0);
let stories =
1 + (rng_unit(&mut state) * (params.max_stories.max(1) as f32)) as u32;
blocks.extend(generate_building(&BuildingParams {
min_corner: Vec3::new(x0 + road, ground_top, z0 + road),
footprint,
stories: stories.max(1),
story_height: params.story_height,
wall_thickness: params.wall_thickness,
floor_thickness,
window_spacing: params.window_spacing,
floor_material: params.floor_material,
wall_material: params.wall_material,
stair_material: (params.wall_material + 3) % PROTOTYPE_MATERIAL_COUNT,
}));
}
}
}
blocks
}
fn block_world_aabb(block: &PlacedBlock) -> (Vec3, Vec3) {
let full = shape_full_extent(block.shape);
let half = Vec3::new(
block.scale.x * full.x * 0.5,
block.scale.y * full.y * 0.5,
block.scale.z * full.z * 0.5,
);
(block.translation - half, block.translation + half)
}
fn aabb_overlap(a: (Vec3, Vec3), b: (Vec3, Vec3)) -> bool {
let margin = 0.02;
a.0.x < b.1.x - margin
&& a.1.x > b.0.x + margin
&& a.0.y < b.1.y - margin
&& a.1.y > b.0.y + margin
&& a.0.z < b.1.z - margin
&& a.1.z > b.0.z + margin
}
fn vec_min(a: Vec3, b: Vec3) -> Vec3 {
Vec3::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z))
}
fn vec_max(a: Vec3, b: Vec3) -> Vec3 {
Vec3::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z))
}
fn entity_world_aabb(world: &World, entity: Entity) -> Option<(Vec3, Vec3)> {
let bounding_volume = world.core.get_bounding_volume(entity)?;
let global = world.core.get_global_transform(entity)?;
let corners = bounding_volume.transform(&global.0).obb.get_corners();
let mut min = corners[0];
let mut max = corners[0];
for corner in corners {
min = vec_min(min, corner);
max = vec_max(max, corner);
}
Some((min, max))
}
fn find_clear_offset(
editor_world: &EditorWorld,
world: &World,
blocks: &[PlacedBlock],
step: f32,
) -> Vec3 {
let mut batch_min = None;
let mut batch_max = Vec3::zeros();
for block in blocks {
let (min, max) = block_world_aabb(block);
match batch_min {
None => {
batch_min = Some(min);
batch_max = max;
}
Some(current) => {
batch_min = Some(vec_min(current, min));
batch_max = vec_max(batch_max, max);
}
}
}
let Some(batch_min) = batch_min else {
return Vec3::zeros();
};
let existing: Vec<(Vec3, Vec3)> = world
.core
.query_entities(nightshade::ecs::world::BOUNDING_VOLUME)
.filter(|entity| !editor_world.resources.editor_scene.is_scaffolding(*entity))
.filter_map(|entity| entity_world_aabb(world, entity))
.collect();
if existing.is_empty() {
return Vec3::zeros();
}
let size = batch_max - batch_min;
let clearance = step.max(1.0);
let cell_x = size.x + clearance;
let cell_z = size.z + clearance;
for ring in 0..=24_i32 {
for column in -ring..=ring {
for row in -ring..=ring {
if column.abs() != ring && row.abs() != ring {
continue;
}
let offset = Vec3::new(column as f32 * cell_x, 0.0, row as f32 * cell_z);
let shifted = (batch_min + offset, batch_max + offset);
if !existing.iter().any(|other| aabb_overlap(shifted, *other)) {
return offset;
}
}
}
}
Vec3::zeros()
}
pub enum BuildCommand {
PlaceBlocks(Vec<PlacedBlock>),
Building(BuildingParams),
Tower(TowerParams),
Stairs(StairsParams),
Columns(ColumnsParams),
Perimeter(PerimeterParams),
CityBlock(CityBlockParams),
Courtyard(CourtyardParams),
Wfc(WfcParams),
WfcCity(WfcCityParams),
}
pub fn apply_command(editor_world: &mut EditorWorld, world: &mut World, command: BuildCommand) {
let relocate = !matches!(command, BuildCommand::PlaceBlocks(_));
let (mut blocks, label): (Vec<PlacedBlock>, &str) = match command {
BuildCommand::PlaceBlocks(blocks) => (blocks, "Place blocks"),
BuildCommand::Building(params) => (generate_building(¶ms), "Generate building"),
BuildCommand::Tower(params) => (generate_tower(¶ms), "Generate tower"),
BuildCommand::Stairs(params) => (generate_stairs(¶ms), "Generate stairs"),
BuildCommand::Columns(params) => (generate_columns(¶ms), "Generate columns"),
BuildCommand::Perimeter(params) => (generate_perimeter(¶ms), "Generate perimeter"),
BuildCommand::CityBlock(params) => (generate_city_block(¶ms), "Generate city block"),
BuildCommand::Courtyard(params) => (generate_courtyard(¶ms), "Generate courtyard"),
BuildCommand::Wfc(params) => (generate_wfc(¶ms), "Generate WFC level"),
BuildCommand::WfcCity(params) => (generate_wfc_city(¶ms), "Generate WFC city"),
};
if relocate {
let step = snap_step(editor_world);
let offset = find_clear_offset(editor_world, world, &blocks, step);
if offset != Vec3::zeros() {
for block in &mut blocks {
block.translation += offset;
}
}
}
place_blocks(editor_world, world, &blocks, label);
}
pub fn snap_selection_to_grid(editor_world: &mut EditorWorld, world: &mut World) {
let step = snap_step(editor_world);
let mut targets: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
if targets.is_empty()
&& let Some(entity) = editor_world.resources.ui.selected_entity
{
targets.push(entity);
}
for entity in targets {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.translation = snap_translation(transform.translation, step);
}
mark_local_transform_dirty(world, entity);
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
}
}
pub fn apply_active_to_selection(editor_world: &mut EditorWorld, world: &mut World) {
let index = editor_world.resources.build.active_palette;
let mut targets: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
if targets.is_empty()
&& let Some(entity) = editor_world.resources.ui.selected_entity
{
targets.push(entity);
}
for entity in targets {
apply_to_entity(world, index, entity);
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
}
}
pub fn apply_to_entity(world: &mut World, index: usize, entity: Entity) {
if world.core.get_material_ref(entity).is_none() {
return;
}
let name = register_palette_material(world, index);
world.core.set_material_ref(entity, MaterialRef::new(name));
world.resources.mesh_render_state.request_full_rebuild();
}
fn register_palette_material(world: &mut World, index: usize) -> String {
let entry = &PALETTE[index % PALETTE.len()];
let material = build_material(&entry.swatch);
let name = entry.material_name.to_string();
material_registry_insert(
&mut world.resources.assets.material_registry,
name.clone(),
material,
);
if let Some(®istry_index) = world
.resources
.assets
.material_registry
.registry
.name_to_index
.get(&name)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
registry_index,
);
}
name
}
fn build_material(swatch: &Swatch) -> Material {
match swatch {
Swatch::Prototype { key } => Material {
base_color: [0.95, 0.95, 0.95, 1.0],
base_texture: Some((*key).to_string()),
base_texture_transform: TextureTransform {
scale: [PROTOTYPE_TILING, PROTOTYPE_TILING],
..Default::default()
},
roughness: 0.85,
metallic: 0.0,
..Default::default()
},
Swatch::Color {
rgb,
roughness,
metallic,
} => Material {
base_color: [rgb[0], rgb[1], rgb[2], 1.0],
roughness: *roughness,
metallic: *metallic,
..Default::default()
},
Swatch::Emissive { rgb, strength } => Material {
base_color: [rgb[0], rgb[1], rgb[2], 1.0],
emissive_factor: *rgb,
emissive_strength: *strength,
roughness: 0.6,
metallic: 0.0,
..Default::default()
},
}
}