initial commit

This commit is contained in:
Viv Lim 2022-12-20 17:47:27 -08:00
commit ebec2b8711
50 changed files with 10018 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
.idea/*
!/.idea/runConfigurations
.DS_Store

4195
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[workspace]
members = [
"app",
"common",
]
resolver = "2" # need to use resolver = "2" to be able to build wgpu-hal

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

24
app/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{
"type": "lldb",
"request": "launch",
"name": "Debug executable",
"cargo": {
"args": [
"build",
"--bin=voxel-level-editor"
],
"filter": {
"name": "voxel-level-editor",
"kind": "bin"
}
},
"args": [
],
"cwd": "${workspaceFolder}"
}]
}

3418
app/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

20
app/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "voxel-level-editor"
version = "0.1.0"
edition = "2021"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
common = { path = "../common", features = [ "two_dimensional", "three_dimensional", "serde", "bevy", "import_dot_vox" ] }
bevy = { version = "0.8", default-features = false, features = ["bevy_winit", "bevy_render", "render", "png", "x11"] }
block-mesh = "0.2.0"
bevy_egui = "0.15.1"
egui = "0.18"
egui_extras = "0.18"
smooth-bevy-cameras = "0.6.0"
itertools = "0.10"
rfd = "0.8"
async-channel = "1.6.1"
enum-map = "2.4" # syntax_highlighting dep, possibly redundant with strum?

BIN
app/res/sample_level_1.vox Normal file

Binary file not shown.

BIN
app/res/sample_level_2.vox Normal file

Binary file not shown.

BIN
app/res/sample_level_3.vox Normal file

Binary file not shown.

View File

@ -0,0 +1,15 @@
use bevy::prelude::Component;
use common::space::three_dimensional::{traits::VoxelContainer, vec3generic::Vec3Generic};
use crate::voxels::bool_voxel::BoolVoxel;
#[derive(Debug, Default, Component)]
pub struct VoxelCursorLayer {
pub position: Vec3Generic<i32>,
}
impl VoxelContainer<BoolVoxel> for VoxelCursorLayer {
fn get_voxel_at_pos(&self, pos: Vec3Generic<i32>) -> Option<BoolVoxel> {
Some(BoolVoxel(self.position == pos))
}
}

View File

@ -0,0 +1,17 @@
use std::sync::Arc;
use bevy::prelude::Component;
use common::space::three_dimensional::{traits::VoxelContainer, vec3generic::Vec3Generic};
use crate::voxels::bool_voxel::BoolVoxel;
#[derive(Component)]
pub struct VoxelFnLayer {
pub is_solid: Arc<dyn Fn(Vec3Generic<i32>) -> bool + Send + Sync>,
}
impl VoxelContainer<BoolVoxel> for VoxelFnLayer {
fn get_voxel_at_pos(&self, pos: Vec3Generic<i32>) -> Option<BoolVoxel> {
Some(BoolVoxel((self.is_solid)(pos)))
}
}

View File

@ -0,0 +1,5 @@
pub mod cursor_layer;
pub mod fn_layer;
pub mod mutable_mesh;
pub mod named;
pub mod property_pane;

View File

@ -0,0 +1,7 @@
use bevy::prelude::Component;
#[derive(Component)]
pub struct MutableMesh {
pub current: bool,
pub visible: bool,
}

View File

@ -0,0 +1,6 @@
use bevy::prelude::Component;
#[derive(Component)]
pub struct Named {
pub name: String,
}

View File

@ -0,0 +1,18 @@
use bevy::prelude::Component;
#[derive(Component)]
pub struct PropertyPane {
pub title: String,
pub closable: bool,
pub is_open: bool,
}
impl PropertyPane {
pub fn new(title: String, closable: bool) -> Self {
PropertyPane {
title,
closable,
is_open: true,
}
}
}

166
app/src/main.rs Normal file
View File

@ -0,0 +1,166 @@
use bevy::render::mesh::MeshVertexAttribute;
use bevy::render::render_resource::VertexFormat;
use bevy::tasks::TaskPoolBuilder;
use bevy_egui::EguiPlugin;
use bevy::{
pbr::wireframe::{WireframeConfig, WireframePlugin},
prelude::*,
render::settings::WgpuSettings,
};
use common::space::level_tile::LevelTile;
use common::space::three_dimensional::hash_map_container::VoxelHashMapLayer;
use common::space::three_dimensional::vec3generic::Vec3Generic;
use common::space::two_dimensional::depth_tiles::DepthTileContainer;
use components::cursor_layer::VoxelCursorLayer;
use components::property_pane::PropertyPane;
use smooth_bevy_cameras::controllers::orbit::{
OrbitCameraBundle, OrbitCameraController, OrbitCameraPlugin,
};
use smooth_bevy_cameras::{LookTransform, LookTransformPlugin};
use systems::{generic_command_queue::CommandQueue, layer_spawner::LayerSpawnerCommand, ui::ui_spawner::{UiSpawnerCommand, UiSpawnerState}};
use voxels::bool_voxel::BoolVoxel;
use voxels::mesh::into_domain;
mod components;
mod systems;
mod voxels;
pub const ATTRIBUTE_POSITION: MeshVertexAttribute =
MeshVertexAttribute::new("Position", 88238261, VertexFormat::Float32x3);
pub const ATTRIBUTE_NORMAL: MeshVertexAttribute =
MeshVertexAttribute::new("Normal", 508828962, VertexFormat::Float32x3);
pub const ATTRIBUTE_UV: MeshVertexAttribute =
MeshVertexAttribute::new("UV", 1982256274, VertexFormat::Float32x2);
fn main() {
App::new()
.insert_resource(WgpuSettings {
// features: WgpuFeatures::POLYGON_MODE_LINE,
..Default::default()
})
.insert_resource(Msaa { samples: 4 })
.insert_resource(CommandQueue::<LayerSpawnerCommand>::default())
.insert_resource(CommandQueue::<UiSpawnerCommand>::default())
.insert_resource(UiSpawnerState::default())
.insert_resource(TaskPoolBuilder::new().num_threads(1).build())
.add_plugins(DefaultPlugins)
.add_plugin(WireframePlugin)
.add_plugin(EguiPlugin)
.add_plugin(LookTransformPlugin)
.add_plugin(OrbitCameraPlugin {
override_input_system: false,
})
.add_system(systems::ui::layers::layer_ui)
.add_system(systems::ui::properties::properties_ui)
.add_system(systems::ui::properties::clean_up_closed_panes)
.add_system(
systems::mutable_mesh_refresher::mutable_mesh_refresher::<
VoxelHashMapLayer<LevelTile>,
LevelTile,
>,
)
.add_system(
systems::mutable_mesh_refresher::mutable_mesh_refresher::<
DepthTileContainer<LevelTile>,
LevelTile,
>,
)
.add_system(
systems::mutable_mesh_refresher::mutable_mesh_refresher::<VoxelCursorLayer, BoolVoxel>,
)
.add_system(move_camera_system)
.add_system(look_at_cursor_system)
.add_system(systems::layer_spawner::layer_spawner)
.add_system(systems::ui::ui_spawner::ui_spawner)
.add_startup_system(setup)
.run();
}
fn setup(
mut commands: Commands,
mut wireframe_config: ResMut<WireframeConfig>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
wireframe_config.global = false;
commands.spawn_bundle(PointLightBundle {
transform: Transform::from_translation(Vec3::new(25.0, 25.0, 25.0)),
point_light: PointLight {
range: 200.0,
intensity: 8000.0,
..Default::default()
},
..Default::default()
});
let _camera_bundle = commands
.spawn_bundle(OrbitCameraBundle::new(
OrbitCameraController::default(),
Vec3::new(-10.0, 21.0, -10.0),
Vec3::new(0.0, 0.0, 0.0),
))
.insert_bundle(Camera3dBundle::default())
.insert(PropertyPane::new("Camera".to_string(), false));
/*.insert_bundle(Camera3dBundle {
projection: OrthographicProjection {
scale: 1.0,
scaling_mode: ScalingMode::FixedVertical(32.0),
..default()
}.into(),
transform: Transform::from_translation(Vec3::new(50.0, 15.0, 50.0))
.looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
..Default::default()
}); */
voxels::layers::container_to_layer_component(
"cursor".to_string(),
VoxelCursorLayer {
position: Vec3Generic { x: 5, y: 5, z: 3 },
},
StandardMaterial::from(Color::rgba(10.0, 0.0, 0.0, 0.5)),
&mut meshes,
&mut commands,
&mut materials,
);
// voxels::layers::container_to_layer_component(
// "bounds".to_string(),
// VoxelFnLayer { is_solid: Arc::new(|p: Vec3Generic<i32>| p.x % 3 == 0) },
// StandardMaterial::from(Color::rgba(0.0, 3.0, 3.0, 0.5)),
// &mut meshes,
// &mut commands,
// &mut materials);
}
fn move_camera_system(mut cameras: Query<&mut LookTransform>) {
for mut c in cameras.iter_mut() {
c.target += Vec3::new(1.0, 1.0, 1.0);
}
}
fn look_at_cursor_system(
mut cameras: Query<&mut LookTransform>,
cursor_mesh: Query<(&VoxelCursorLayer, &Handle<Mesh>)>,
) {
match cursor_mesh.get_single() {
Ok((cursor, _cursor_mesh)) => {
let cursor_pos = into_domain(/*unused?*/ 0, cursor.position.into());
// assume cursor is a single voxel size
let cursor_size = into_domain(/*unused?*/ 0, [1, 1, 1]);
// add half the cursor size so that the look target is in the middle of the cursor
let look_target = cursor_pos + (cursor_size); //((cursor_pos * 2.0) + 1.0) / 2.0;
let mut camera = cameras.single_mut();
camera.target = Vec3 {
x: look_target.x,
y: look_target.y,
z: look_target.z,
};
}
Err(_) => (), // maybe cursor mesh was hidden?
}
}
const SAMPLE_WORLD: &[u8] = include_bytes!("../res/sample_level_2.vox");
const SAMPLE_WORLD_OLD: &[u8] = include_bytes!("../res/sample_level_1.vox");

View File

@ -0,0 +1,44 @@
use std::collections::VecDeque;
use bevy::prelude::Resource;
/// Command queue which allows both synchronous and asynchronous queuing.
#[derive(Resource)]
pub struct CommandQueue<T> {
sync_commands: VecDeque<T>,
async_sender: async_channel::Sender<T>,
async_receiver: async_channel::Receiver<T>,
}
impl<T> CommandQueue<T> {
pub fn add(&mut self, command: T) {
self.sync_commands.push_back(command)
}
pub fn get_async_sender(&self) -> async_channel::Sender<T> {
self.async_sender.clone()
}
pub fn get_commands(&mut self) -> Vec<T> {
let mut commands = vec![];
while let Ok(command) = self.async_receiver.try_recv() {
commands.push(command);
}
commands.extend(self.sync_commands.drain(0..self.sync_commands.len()));
commands
}
}
impl<T> Default for CommandQueue<T> {
fn default() -> Self {
let (async_sender, async_receiver) = async_channel::unbounded::<T>();
Self {
sync_commands: Default::default(),
async_sender,
async_receiver,
}
}
}

View File

@ -0,0 +1,128 @@
use common::space::{
level_tile::LevelTile, three_dimensional::hash_map_container::VoxelHashMapLayer,
two_dimensional::depth_tiles::{DepthTileContainer, DepthTile},
};
use common::strum::IntoEnumIterator;
use std::collections::VecDeque;
use bevy::{prelude::*, tasks::TaskPool};
use common::space::two_dimensional::depth_tiles::Direction;
use super::{generic_command_queue::CommandQueue, ui::ui_spawner::{UiSpawnerCommand, build_message_box_command, build_command, EditorPopup}};
pub fn layer_spawner(
mut spawner_commands: ResMut<CommandQueue<LayerSpawnerCommand>>,
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
mut tile_hashmap_layer_query: Query<&mut VoxelHashMapLayer<LevelTile>>,
ui_spawner_commands: ResMut<CommandQueue<UiSpawnerCommand>>,
pool: Res<TaskPool>,
) {
for command in spawner_commands.get_commands() {
match command {
LayerSpawnerCommand::FromVoxData { name, data } => {
match common::space::three_dimensional::hash_map_container::import_from_bytes::<
LevelTile,
>(&data)
{
Ok(voxels) => {
crate::voxels::layers::container_to_layer_component(
name,
voxels,
StandardMaterial::from(Color::rgb(5.0, 5.0, 5.0)),
&mut meshes,
&mut commands,
&mut materials,
);
}
Err(e) => {
println!("Couldn't load file {}: {:?}", name, e)
}
}
}
LayerSpawnerCommand::FromTileProjectedLayer {
name,
source_layer,
direction,
} => {
if let Ok(layer) = tile_hashmap_layer_query.get_mut(source_layer) {
let layer = layer.into_inner();
let tiles = common::space::three_dimensional::project_to_2d::create_tilemap(
layer, direction, 32, 32, 32,
);
let tile_map_layer = DepthTileContainer::<LevelTile> { tiles };
crate::voxels::layers::container_to_layer_component(
name,
tile_map_layer,
StandardMaterial::from(Color::rgb(0.0, 5.0, 0.0)),
&mut meshes,
&mut commands,
&mut materials,
);
}
}
LayerSpawnerCommand::ExportProjectedTilesToml { name, source_layer } => {
let sender = ui_spawner_commands.get_async_sender();
if let Ok(layer) = tile_hashmap_layer_query.get_mut(source_layer) {
let mut layer = layer.into_inner().clone();
pool.spawn(async move {
match common::space::level_serde::create_export_map(&mut layer) {
Ok(map) => {
match common::ron::to_string(&map) {
Ok(map_ron) => {
sender.send(build_command(EditorPopup {
title: "exported".to_string(),
data: map_ron,
open: true
})).await;
},
Err(e) => {
let (cmd, recv) = build_message_box_command("error".to_string(), format!("serialization failed: {:?}", e), vec![":(", "show Debug"], true);
sender.send(cmd).await;
if let Ok(button_pressed) = recv.recv().await {
if button_pressed == "show Debug" {
sender.send(build_command(EditorPopup {
title: "Debug of export".to_string(),
data: format!("{:?}", map),
open: true
})).await;
}
}
}
}
},
Err(e) => {
let (cmd, recv) = build_message_box_command("error".to_string(), format!("export failed: {:?}", e), vec![":("], true);
sender.send(cmd).await;
},
}
}).detach();
}
},
}
}
}
pub enum LayerSpawnerCommand {
FromVoxData {
name: String,
data: Vec<u8>,
},
FromTileProjectedLayer {
name: String,
source_layer: Entity,
direction: common::space::two_dimensional::depth_tiles::Direction,
},
ExportProjectedTilesToml {
name: String,
source_layer: Entity,
}
}

4
app/src/systems/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod layer_spawner;
pub mod mutable_mesh_refresher;
pub mod ui;
pub mod generic_command_queue;

View File

@ -0,0 +1,39 @@
use bevy::prelude::{Commands, Component, Entity, Mesh, Query, ResMut};
use block_mesh::Voxel;
use common::space::three_dimensional::traits::DefaultVoxel;
use crate::{components::mutable_mesh::MutableMesh, voxels::layers::traits::VoxelLayer};
pub fn mutable_mesh_refresher<TContainer, TVoxel>(
mut commands: Commands,
mut query: Query<(Entity, &mut MutableMesh, &TContainer)>,
mesh_query: Query<&Handle<Mesh>>,
mut meshes: ResMut<Assets<Mesh>>,
) where
TContainer: VoxelLayer<TVoxel> + Component,
TVoxel: Voxel + DefaultVoxel + Copy,
{
// for (e, mut mutable_mesh, container) in query.iter_mut().filter(|(_, mm, _)| mm.current == false){
for (e, mut mutable_mesh, container) in query.iter_mut() {
if !mutable_mesh.current {
let mut commands = commands.entity(e);
match mutable_mesh.visible {
true => {
if mesh_query.contains(e) {
commands.remove::<Handle<Mesh>>();
}
let new_mesh = container.generate_simple_mesh(&mut meshes);
commands.insert(new_mesh);
}
false => {
if mesh_query.contains(e) {
commands.remove::<Handle<Mesh>>();
}
}
};
mutable_mesh.current = true;
}
}
}

View File

@ -0,0 +1,161 @@
use crate::{
components::{
cursor_layer::VoxelCursorLayer, mutable_mesh::MutableMesh, named::Named,
property_pane::PropertyPane,
},
systems::{layer_spawner::{LayerSpawnerCommand}, generic_command_queue::CommandQueue, ui::ui_spawner::{UiSpawnerCommand, build_message_box_command, EditorPopup, build_command}},
};
use bevy::{
prelude::{Camera, Commands, Entity, Query, Res, ResMut},
render::camera::Projection,
tasks::TaskPool,
};
use bevy_egui::EguiContext;
use egui_extras::{Size, TableBuilder};
use itertools::Itertools;
use smooth_bevy_cameras::LookTransform;
pub fn layer_ui(
mut commands: Commands,
layer_spawner_commands: ResMut<CommandQueue<LayerSpawnerCommand>>,
ui_spawner_commands: ResMut<CommandQueue<UiSpawnerCommand>>,
mut egui_context: ResMut<EguiContext>,
mut camera_query: Query<(&mut Camera, &mut Projection, &mut LookTransform)>,
mut layer_query: Query<(
&mut Named,
Entity,
&mut MutableMesh,
Option<&mut VoxelCursorLayer>,
)>,
property_pane_query: Query<&mut PropertyPane>,
pool: Res<TaskPool>,
) {
egui::Window::new("Layers")
.resizable(true)
.vscroll(true)
.show(egui_context.ctx_mut(), |ui| {
let (_camera, _projection, _look_transform) = camera_query.single_mut();
TableBuilder::new(ui)
.striped(true)
.scroll(true)
.column(Size::initial(200.0).at_least(40.0))
.column(Size::initial(60.0))
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Name");
});
header.col(|ui| {
ui.heading("Visible");
});
})
.body(|mut body| {
// draw layer controls
for layer_query_result in layer_query
.iter_mut()
// sort by name
.sorted_by(|(left_named, ..), (right_named, ..)| {
left_named.name.cmp(&right_named.name)
})
{
let (named, entity, mut mutable_mesh, _cursor_layer) = layer_query_result;
body.row(20.0, |mut row| {
row.col(|ui| {
if ui.button(&named.name).clicked()
&& !property_pane_query.contains(entity)
{
commands.entity(entity).insert(PropertyPane::new(
format!("Layer - {}", named.name),
true,
));
}
});
row.col(|ui| {
if ui.checkbox(&mut mutable_mesh.visible, "").clicked() {
mutable_mesh.current = false;
}
});
});
// if let Some(mut cursor_layer) = cursor_layer {
// ui.horizontal(|ui| {
// if ui.add(egui::DragValue::new(&mut cursor_layer.position.x).speed(1).prefix("x:")).changed() ||
// ui.add(egui::DragValue::new(&mut cursor_layer.position.y).speed(1).prefix("y:")).changed() ||
// ui.add(egui::DragValue::new(&mut cursor_layer.position.z).speed(1).prefix("z:")).changed() {
// // invalidate the mesh so it is redrawn
// mutable_mesh.current = false;
// }
// });
// }
//ui.label(format!("size: {}x{}x{}", layer.size.x, layer.size.y, layer.size.z));
}
});
ui.separator();
if ui.button("Load .vox").clicked() {
let sender = layer_spawner_commands.get_async_sender();
pool.spawn(async move {
if let Some(path) = rfd::AsyncFileDialog::new()
.add_filter("MagicaVoxel .vox", &["vox"])
.set_directory(".")
.pick_file()
.await
{
let data = path.read().await;
sender
.send(LayerSpawnerCommand::FromVoxData {
name: path.file_name(),
data,
})
.await
.unwrap();
}
})
.detach();
}
if ui.button("Popup test").clicked() {
let sender = ui_spawner_commands.get_async_sender();
pool.spawn(async move {
println!("opening 1st message box");
let (m, recv) = build_message_box_command("test 1".to_string(), "hello! i'm a test dialog".to_string(), vec!["ok cool", "so", "editor"], true);
sender.send(m).await.unwrap();
println!("awaiting click");
if let Ok(clicked) = recv.recv().await {
println!("the button {} was clicked", clicked);
if clicked == "editor" {
sender.send(build_command(EditorPopup {
title: "editor".to_string(),
data: "hello world".to_string(),
open: true
}
)).await.unwrap();
}
else {
let (m, _) = build_message_box_command("test 2".to_string(), format!("you clicked {}", clicked), vec!["ok."], true);
sender.send(m).await.unwrap();
}
} else {
println!("no button was clicked");
}
})
.detach();
}
/*
for dir in Direction::iter() {
if ui.add(Button::new(format!("project {:?}", dir))).clicked() {
// get the right layer named 'world'.
let layer = layer_query.iter().filter(|(_, named, _)| named.name == "world").last();
};
} */
});
}

View File

@ -0,0 +1,5 @@
pub mod layers;
pub mod properties;
pub mod util;
pub mod syntax_highlighting;
pub mod ui_spawner;

View File

@ -0,0 +1,168 @@
use core::fmt;
use std::rc::Rc;
use bevy::{
prelude::{Camera, Commands, Component, Entity, GlobalTransform, Query, ResMut, Transform},
render::camera::Projection,
};
use bevy_egui::EguiContext;
use block_mesh::Voxel;
use common::space::{
level_tile::LevelTile,
three_dimensional::{
hash_map_container::VoxelHashMapLayer,
traits::{DefaultVoxel, VoxelContainer},
},
two_dimensional::depth_tiles::Direction,
};
use egui::{mutex::Mutex, Button, CollapsingHeader, Ui};
use smooth_bevy_cameras::LookTransform;
use common::strum::IntoEnumIterator;
use crate::{
components::{mutable_mesh::MutableMesh, named::Named, property_pane::PropertyPane},
systems::{layer_spawner::{LayerSpawnerCommand}, generic_command_queue::CommandQueue},
voxels::layers::traits::VoxelLayer,
};
use super::util::{bevy_quat_controls, bevy_vec3_controls};
pub fn properties_ui(
commands: Commands,
layer_spawner_commands: ResMut<CommandQueue<LayerSpawnerCommand>>,
mut egui_context: ResMut<EguiContext>,
mut property_pane_query: Query<(&mut PropertyPane, Entity)>,
mut camera_query: Query<(&mut Camera, &mut Projection, &mut LookTransform)>,
mut transform_query: Query<(&mut Transform, &mut GlobalTransform)>,
mut tile_hashmap_layer_query: Query<(
&mut VoxelHashMapLayer<LevelTile>,
&mut Named,
&mut MutableMesh,
)>,
) {
// in case we want to send commands from multiple places in the ui
let _commands = Rc::new(Mutex::new(commands));
let layer_spawner_commands = Rc::new(Mutex::new(layer_spawner_commands));
for (mut property_pane, entity) in property_pane_query.iter_mut() {
egui::Window::new(&property_pane.title)
.open(&mut property_pane.is_open)
.min_width(400.0)
.resizable(true)
.vscroll(true)
.show(egui_context.ctx_mut(), |ui| {
voxel_layer_properties(
layer_spawner_commands.clone(),
ui,
entity,
&mut tile_hashmap_layer_query,
);
if let Ok((_camera, projection, mut look_transform)) = camera_query.get_mut(entity)
{
let _heading =
CollapsingHeader::new("Camera")
.default_open(true)
.show(ui, |ui| {
match projection.into_inner() {
Projection::Perspective(_perspective) => {}
Projection::Orthographic(orthographic) => {
ui.add(
egui::DragValue::new(&mut orthographic.scale)
.speed(1)
.prefix("orthographic scale:"),
);
}
}
ui.add(
egui::DragValue::new(&mut look_transform.eye.x)
.speed(0.2)
.prefix("eye x:"),
);
ui.add(
egui::DragValue::new(&mut look_transform.eye.y)
.speed(0.2)
.prefix("eye y:"),
);
ui.add(
egui::DragValue::new(&mut look_transform.eye.z)
.speed(0.2)
.prefix("eye z:"),
);
});
}
if let Ok((mut transform, global_transform)) = transform_query.get_mut(entity) {
let _heading =
CollapsingHeader::new("Transform")
.default_open(true)
.show(ui, |ui| {
bevy_vec3_controls(ui, &mut transform.translation, "pos");
bevy_quat_controls(ui, &mut transform.rotation, "rot");
bevy_vec3_controls(ui, &mut transform.scale, "scale");
});
let _heading = CollapsingHeader::new("GlobalTransform")
.default_open(true)
.show(ui, |ui| {
let (mut scale, mut rotation, mut translation) =
global_transform.to_scale_rotation_translation();
bevy_vec3_controls(ui, &mut translation, "pos");
bevy_quat_controls(ui, &mut rotation, "rot");
bevy_vec3_controls(ui, &mut scale, "scale");
});
}
});
}
}
fn voxel_layer_properties<TLayer, TVoxel>(
layer_spawner_commands: Rc<Mutex<ResMut<CommandQueue<LayerSpawnerCommand>>>>,
ui: &mut Ui,
entity: Entity,
layer_query: &mut Query<(&mut TLayer, &mut Named, &mut MutableMesh)>,
) where
TLayer: VoxelLayer<TVoxel> + VoxelContainer<TVoxel> + Component,
TVoxel: Voxel + DefaultVoxel + Copy + Send + Sync + fmt::Debug,
{
if let Ok((layer, named, _mutable_mesh)) = layer_query.get_mut(entity) {
let _layer = layer.into_inner();
CollapsingHeader::new("Generic Voxel Layer")
.default_open(true)
.show(ui, |ui| {
ui.horizontal(|ui| {
for dir in Direction::iter() {
if ui.add(Button::new(format!("project {:?}", dir))).clicked() {
layer_spawner_commands.lock().add(
LayerSpawnerCommand::FromTileProjectedLayer {
name: format!("{} - {:?} proj", named.name, dir),
source_layer: entity,
direction: dir,
},
)
};
}
});
ui.separator();
if ui.button("Export projected tiles").clicked() {
layer_spawner_commands.lock().add(
LayerSpawnerCommand::ExportProjectedTilesToml { name: named.name.to_string(), source_layer: entity }
);
}
});
}
}
pub fn clean_up_closed_panes(
mut commands: Commands,
mut property_pane_query: Query<(&mut PropertyPane, Entity)>,
) {
for (mut pane, entity) in property_pane_query.iter_mut() {
if pane.closable && !pane.is_open {
commands.entity(entity).remove::<PropertyPane>();
} else if !pane.closable && !pane.is_open {
// reopen it. don't allow closing this one
pane.is_open = true;
}
}
}

View File

@ -0,0 +1,508 @@
// This is just copied from the egui sample
// because it's not actually packed in a crate...
// https://github.com/emilk/egui/discussions/1628#discussioncomment-2751231
// source: https://raw.githubusercontent.com/emilk/egui/master/crates/egui_demo_lib/src/syntax_highlighting.rs
use egui::text::LayoutJob;
/// View some code with syntax highlighting and selection.
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
let language = "rs";
let theme = CodeTheme::from_memory(ui.ctx());
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
let layout_job = highlight(ui.ctx(), &theme, string, language);
// layout_job.wrap.max_width = wrap_width; // no wrapping
ui.fonts().layout_job(layout_job)
};
ui.add(
egui::TextEdit::multiline(&mut code)
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(1)
.lock_focus(true)
.layouter(&mut layouter),
);
}
/// Memoized Code highlighting
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
self.highlight(theme, code, lang)
}
}
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
let mut memory = ctx.memory();
let highlight_cache = memory.caches.cache::<HighlightCache>();
highlight_cache.get((theme, code, language))
}
// ----------------------------------------------------------------------------
#[cfg(not(feature = "syntect"))]
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(enum_map::Enum)]
enum TokenType {
Comment,
Keyword,
Literal,
StringLiteral,
Punctuation,
Whitespace,
}
#[cfg(feature = "syntect")]
#[derive(Clone, Copy, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum SyntectTheme {
Base16EightiesDark,
Base16MochaDark,
Base16OceanDark,
Base16OceanLight,
InspiredGitHub,
SolarizedDark,
SolarizedLight,
}
#[cfg(feature = "syntect")]
impl SyntectTheme {
fn all() -> impl ExactSizeIterator<Item = Self> {
[
Self::Base16EightiesDark,
Self::Base16MochaDark,
Self::Base16OceanDark,
Self::Base16OceanLight,
Self::InspiredGitHub,
Self::SolarizedDark,
Self::SolarizedLight,
]
.iter()
.copied()
}
fn name(&self) -> &'static str {
match self {
Self::Base16EightiesDark => "Base16 Eighties (dark)",
Self::Base16MochaDark => "Base16 Mocha (dark)",
Self::Base16OceanDark => "Base16 Ocean (dark)",
Self::Base16OceanLight => "Base16 Ocean (light)",
Self::InspiredGitHub => "InspiredGitHub (light)",
Self::SolarizedDark => "Solarized (dark)",
Self::SolarizedLight => "Solarized (light)",
}
}
fn syntect_key_name(&self) -> &'static str {
match self {
Self::Base16EightiesDark => "base16-eighties.dark",
Self::Base16MochaDark => "base16-mocha.dark",
Self::Base16OceanDark => "base16-ocean.dark",
Self::Base16OceanLight => "base16-ocean.light",
Self::InspiredGitHub => "InspiredGitHub",
Self::SolarizedDark => "Solarized (dark)",
Self::SolarizedLight => "Solarized (light)",
}
}
pub fn is_dark(&self) -> bool {
match self {
Self::Base16EightiesDark
| Self::Base16MochaDark
| Self::Base16OceanDark
| Self::SolarizedDark => true,
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
}
}
}
#[derive(Clone, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct CodeTheme {
dark_mode: bool,
#[cfg(feature = "syntect")]
syntect_theme: SyntectTheme,
#[cfg(not(feature = "syntect"))]
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
}
impl Default for CodeTheme {
fn default() -> Self {
Self::dark()
}
}
impl CodeTheme {
pub fn from_style(style: &egui::Style) -> Self {
if style.visuals.dark_mode {
Self::dark()
} else {
Self::light()
}
}
pub fn from_memory(ctx: &egui::Context) -> Self {
if ctx.style().visuals.dark_mode {
ctx.data()
.get_persisted(egui::Id::new("dark"))
.unwrap_or_else(CodeTheme::dark)
} else {
ctx.data()
.get_persisted(egui::Id::new("light"))
.unwrap_or_else(CodeTheme::light)
}
}
pub fn store_in_memory(self, ctx: &egui::Context) {
if self.dark_mode {
ctx.data().insert_persisted(egui::Id::new("dark"), self);
} else {
ctx.data().insert_persisted(egui::Id::new("light"), self);
}
}
}
#[cfg(feature = "syntect")]
impl CodeTheme {
pub fn dark() -> Self {
Self {
dark_mode: true,
syntect_theme: SyntectTheme::Base16MochaDark,
}
}
pub fn light() -> Self {
Self {
dark_mode: false,
syntect_theme: SyntectTheme::SolarizedLight,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::widgets::global_dark_light_mode_buttons(ui);
for theme in SyntectTheme::all() {
if theme.is_dark() == self.dark_mode {
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
}
}
}
}
#[cfg(not(feature = "syntect"))]
impl CodeTheme {
pub fn dark() -> Self {
let font_id = egui::FontId::monospace(12.0);
use egui::{Color32, TextFormat};
Self {
dark_mode: true,
formats: enum_map::enum_map![
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)),
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)),
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)),
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)),
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY),
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
],
}
}
pub fn light() -> Self {
let font_id = egui::FontId::monospace(12.0);
use egui::{Color32, TextFormat};
Self {
dark_mode: false,
#[cfg(not(feature = "syntect"))]
formats: enum_map::enum_map![
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY),
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)),
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)),
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)),
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY),
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
],
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_top(|ui| {
let selected_id = egui::Id::null();
let mut selected_tt: TokenType = *ui
.data()
.get_persisted_mut_or(selected_id, TokenType::Comment);
ui.vertical(|ui| {
ui.set_width(150.0);
egui::widgets::global_dark_light_mode_buttons(ui);
ui.add_space(8.0);
ui.separator();
ui.add_space(8.0);
ui.scope(|ui| {
for (tt, tt_name) in [
(TokenType::Comment, "// comment"),
(TokenType::Keyword, "keyword"),
(TokenType::Literal, "literal"),
(TokenType::StringLiteral, "\"string literal\""),
(TokenType::Punctuation, "punctuation ;"),
// (TokenType::Whitespace, "whitespace"),
] {
let format = &mut self.formats[tt];
ui.style_mut().override_font_id = Some(format.font_id.clone());
ui.visuals_mut().override_text_color = Some(format.color);
ui.radio_value(&mut selected_tt, tt, tt_name);
}
});
let reset_value = if self.dark_mode {
CodeTheme::dark()
} else {
CodeTheme::light()
};
if ui
.add_enabled(*self != reset_value, egui::Button::new("Reset theme"))
.clicked()
{
*self = reset_value;
}
});
ui.add_space(16.0);
ui.data().insert_persisted(selected_id, selected_tt);
egui::Frame::group(ui.style())
.inner_margin(egui::Vec2::splat(2.0))
.show(ui, |ui| {
// ui.group(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
egui::widgets::color_picker::color_picker_color32(
ui,
&mut self.formats[selected_tt].color,
egui::color_picker::Alpha::Opaque,
);
});
});
}
}
// ----------------------------------------------------------------------------
#[cfg(feature = "syntect")]
struct Highlighter {
ps: syntect::parsing::SyntaxSet,
ts: syntect::highlighting::ThemeSet,
}
#[cfg(feature = "syntect")]
impl Default for Highlighter {
fn default() -> Self {
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
}
}
}
#[cfg(feature = "syntect")]
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
// Fallback:
LayoutJob::simple(
code.into(),
egui::FontId::monospace(14.0),
if theme.dark_mode {
egui::Color32::LIGHT_GRAY
} else {
egui::Color32::DARK_GRAY
},
f32::INFINITY,
)
})
}
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
let syntax = self
.ps
.find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?;
let theme = theme.syntect_theme.syntect_key_name();
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
use egui::text::{LayoutSection, TextFormat};
let mut job = LayoutJob {
text: text.into(),
..Default::default()
};
for line in LinesWithEndings::from(text) {
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
let fg = style.foreground;
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
let italics = style.font_style.contains(FontStyle::ITALIC);
let underline = style.font_style.contains(FontStyle::ITALIC);
let underline = if underline {
egui::Stroke::new(1.0, text_color)
} else {
egui::Stroke::none()
};
job.sections.push(LayoutSection {
leading_space: 0.0,
byte_range: as_byte_range(text, range),
format: TextFormat {
font_id: egui::FontId::monospace(14.0),
color: text_color,
italics,
underline,
..Default::default()
},
});
}
}
Some(job)
}
}
#[cfg(feature = "syntect")]
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
let whole_start = whole.as_ptr() as usize;
let range_start = range.as_ptr() as usize;
assert!(whole_start <= range_start);
assert!(range_start + range.len() <= whole_start + whole.len());
let offset = range_start - whole_start;
offset..(offset + range.len())
}
// ----------------------------------------------------------------------------
#[cfg(not(feature = "syntect"))]
#[derive(Default)]
struct Highlighter {}
#[cfg(not(feature = "syntect"))]
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
// Extremely simple syntax highlighter for when we compile without syntect
let mut job = LayoutJob::default();
while !text.is_empty() {
if text.starts_with("//") {
let end = text.find('\n').unwrap_or(text.len());
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
text = &text[end..];
} else if text.starts_with('"') {
let end = text[1..]
.find('"')
.map(|i| i + 2)
.or_else(|| text.find('\n'))
.unwrap_or(text.len());
job.append(
&text[..end],
0.0,
theme.formats[TokenType::StringLiteral].clone(),
);
text = &text[end..];
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
let end = text[1..]
.find(|c: char| !c.is_ascii_alphanumeric())
.map_or_else(|| text.len(), |i| i + 1);
let word = &text[..end];
let tt = if is_keyword(word) {
TokenType::Keyword
} else {
TokenType::Literal
};
job.append(word, 0.0, theme.formats[tt].clone());
text = &text[end..];
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
let end = text[1..]
.find(|c: char| !c.is_ascii_whitespace())
.map_or_else(|| text.len(), |i| i + 1);
job.append(
&text[..end],
0.0,
theme.formats[TokenType::Whitespace].clone(),
);
text = &text[end..];
} else {
let mut it = text.char_indices();
it.next();
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
job.append(
&text[..end],
0.0,
theme.formats[TokenType::Punctuation].clone(),
);
text = &text[end..];
}
}
job
}
}
#[cfg(not(feature = "syntect"))]
fn is_keyword(word: &str) -> bool {
matches!(
word,
"as" | "async"
| "await"
| "break"
| "const"
| "continue"
| "crate"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
)
}

View File

@ -0,0 +1,141 @@
use bevy_egui::EguiContext;
use common::space::{
level_tile::LevelTile, three_dimensional::hash_map_container::VoxelHashMapLayer,
two_dimensional::depth_tiles::DepthTileContainer,
};
use egui::Ui;
use std::{collections::VecDeque, fmt::Display, future::Future};
use bevy::prelude::*;
use super::{super::generic_command_queue::CommandQueue, syntax_highlighting::{self, CodeTheme}};
pub fn ui_spawner(
mut spawner_commands: ResMut<CommandQueue<UiSpawnerCommand>>,
mut state: ResMut<UiSpawnerState>,
mut egui_context: ResMut<EguiContext>,
) {
for command in spawner_commands.get_commands() {
match command {
UiSpawnerCommand::CreateElement(e) => {
state.elements.push(e)
},
}
}
// remove elements that aren't open
state.elements.retain(|e| e.is_open());
for element in &mut state.elements {
element.display(egui_context.ctx_mut());
}
}
pub enum UiSpawnerCommand {
CreateElement(Box<dyn SpawnedUiElement + Send + Sync>)
}
pub struct EditorPopup {
pub title: String,
pub data: String,
pub open: bool,
}
pub trait SpawnedUiElement {
fn display(&mut self, egui_context: &egui::Context);
fn is_open(&self) -> bool;
}
pub struct MessageBox<T> {
title: String,
body: String,
buttons: Vec<T>,
onclick_sender: async_channel::Sender<T>,
open: bool,
dismiss_on_click: bool,
}
impl<T> SpawnedUiElement for MessageBox<T> where T: Display + Clone {
fn display(&mut self, egui_context: &egui::Context) {
let mut should_close = false;
egui::Window::new(&self.title)
.open(&mut self.open)
.show(egui_context, |ui| {
ui.label(&self.body);
ui.horizontal(|ui| {
for button in self.buttons.iter() {
if ui.button(button.to_string()).clicked() {
self.onclick_sender.try_send(button.clone());
if self.dismiss_on_click {
self.onclick_sender.close();
should_close = true;
}
}
}
});
});
if should_close {
self.open = false;
}
}
fn is_open(&self) -> bool {
self.open
}
}
impl SpawnedUiElement for EditorPopup {
fn display(&mut self, egui_context: &egui::Context) {
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job =
syntax_highlighting::highlight(ui.ctx(), &CodeTheme::dark(), string, "toml");
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
};
egui::Window::new(&self.title)
.open(&mut self.open)
.show(egui_context, |ui|{
egui::ScrollArea::vertical().show(ui, |ui|{
ui.add(egui::TextEdit::multiline(&mut self.data)
.font(egui::TextStyle::Monospace)
.code_editor()
.desired_rows(20)
.lock_focus(true)
.desired_width(400.0)
.layouter(&mut layouter)
)
});
});
}
fn is_open(&self) -> bool {
self.open
}
}
pub fn build_message_box_command<TButton> (title: String, body: String, buttons: Vec<TButton>, dismiss_on_click: bool) -> (UiSpawnerCommand, async_channel::Receiver<TButton>)
where TButton: Display + Send + Sync + Clone + 'static {
let (onclick_sender, onclick_recv) = async_channel::bounded(1);
let mb = MessageBox{
title,
body,
buttons: buttons,
onclick_sender,
open: true,
dismiss_on_click
};
(build_command(mb), onclick_recv)
}
pub fn build_command<T> (element: T) -> UiSpawnerCommand
where T: SpawnedUiElement + Send + Sync + 'static {
UiSpawnerCommand::CreateElement(Box::new(element))
}
#[derive(Default)]
pub struct UiSpawnerState {
elements: Vec<Box<dyn SpawnedUiElement + Send + Sync>>
}

View File

@ -0,0 +1,20 @@
use egui::Ui;
pub fn bevy_vec3_controls(ui: &mut Ui, vec: &mut bevy::prelude::Vec3, label: &str) {
ui.horizontal(|ui| {
ui.label(label);
ui.add(egui::DragValue::new(&mut vec.x).speed(0.2).prefix("x:"));
ui.add(egui::DragValue::new(&mut vec.y).speed(0.2).prefix("y:"));
ui.add(egui::DragValue::new(&mut vec.z).speed(0.2).prefix("z:"));
});
}
pub fn bevy_quat_controls(ui: &mut Ui, quat: &mut bevy::prelude::Quat, label: &str) {
ui.horizontal(|ui| {
ui.label(label);
ui.add(egui::DragValue::new(&mut quat.x).speed(0.2).prefix("x:"));
ui.add(egui::DragValue::new(&mut quat.y).speed(0.2).prefix("y:"));
ui.add(egui::DragValue::new(&mut quat.z).speed(0.2).prefix("z:"));
ui.add(egui::DragValue::new(&mut quat.w).speed(0.2).prefix("w:"));
});
}

View File

@ -0,0 +1,23 @@
use block_mesh::Voxel;
use common::space::three_dimensional::traits::{DefaultVoxel, DefaultVoxelKinds};
#[derive(Copy, Clone)]
pub struct BoolVoxel(pub bool);
impl Voxel for BoolVoxel {
fn get_visibility(&self) -> block_mesh::VoxelVisibility {
match self.0 {
true => block_mesh::VoxelVisibility::Opaque,
false => block_mesh::VoxelVisibility::Empty,
}
}
}
impl DefaultVoxel for BoolVoxel {
fn get_default(kind: DefaultVoxelKinds) -> Self {
match kind {
DefaultVoxelKinds::None => BoolVoxel(false),
DefaultVoxelKinds::Solid => BoolVoxel(true),
}
}
}

View File

@ -0,0 +1,39 @@
use bevy::prelude::*;
use block_mesh::Voxel;
use common::space::three_dimensional::traits::{DefaultVoxel, VoxelContainer};
use crate::components::{mutable_mesh::MutableMesh, named::Named};
use self::traits::VoxelLayer;
pub mod traits;
pub fn container_to_layer_component<'a, TVoxel, TContainer>(
name: String,
container: TContainer,
material: StandardMaterial,
meshes: &mut Assets<Mesh>,
commands: &mut Commands,
materials: &mut Assets<StandardMaterial>,
) -> Entity
where
TVoxel: Voxel + DefaultVoxel + Copy,
TContainer: VoxelLayer<TVoxel> + VoxelContainer<TVoxel> + Component,
{
let mesh = container.generate_simple_mesh(meshes);
commands
.spawn_bundle(PbrBundle {
mesh,
material: materials.add(material),
transform: Transform::identity(),
..Default::default()
})
.insert(container)
.insert(MutableMesh {
current: true,
visible: true,
})
.insert(Named { name })
.id()
}

View File

@ -0,0 +1,89 @@
use bevy::{
prelude::*,
render::{
mesh::{Indices, VertexAttributeValues},
render_resource::PrimitiveTopology,
},
};
use block_mesh::{
ilattice::prelude::Vec3A,
ndshape::{ConstShape, ConstShape3u32},
visible_block_faces, UnitQuadBuffer, Voxel, RIGHT_HANDED_Y_UP_CONFIG,
};
use common::space::three_dimensional::traits::{DefaultVoxel, DefaultVoxelKinds, VoxelContainer};
use crate::voxels::mesh::into_domain;
/// A renderable collection of voxels that have 3-dimensional coordinates, and can be displayed as a single mesh.
pub trait VoxelLayer<TVoxel>
where
TVoxel: Voxel + DefaultVoxel + Copy,
{
fn get_voxel_or_default_at_vec<'a>(&self, pos: Vec3A) -> TVoxel;
fn generate_simple_mesh(&self, meshes: &mut Assets<Mesh>) -> Handle<Mesh>;
}
impl<TVoxel, TContainer> VoxelLayer<TVoxel> for TContainer
where
TVoxel: Voxel + DefaultVoxel + Copy,
TContainer: VoxelContainer<TVoxel>,
{
fn get_voxel_or_default_at_vec<'a>(&self, position: Vec3A) -> TVoxel {
match self.get_voxel_at_pos(position.into()) {
Some(v) => v,
None => DefaultVoxel::get_default(DefaultVoxelKinds::None),
}
}
fn generate_simple_mesh(&self, meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
type SampleShape = ConstShape3u32<34, 34, 34>;
let mut samples: [TVoxel; SampleShape::SIZE as usize] =
[DefaultVoxel::get_default(DefaultVoxelKinds::None); SampleShape::SIZE as usize];
for i in 0u32..(SampleShape::SIZE) {
let p = into_domain(32, SampleShape::delinearize(i));
samples[i as usize] = self.get_voxel_or_default_at_vec(p);
}
let faces = RIGHT_HANDED_Y_UP_CONFIG.faces;
let mut buffer = UnitQuadBuffer::new();
visible_block_faces(
&samples,
&SampleShape {},
[0; 3],
[33; 3],
&faces,
&mut buffer,
);
let num_indices = buffer.num_quads() * 6;
let num_vertices = buffer.num_quads() * 4;
let mut indices = Vec::with_capacity(num_indices);
let mut positions = Vec::with_capacity(num_vertices);
let mut normals = Vec::with_capacity(num_vertices);
for (group, face) in buffer.groups.into_iter().zip(faces.into_iter()) {
for quad in group.into_iter() {
indices.extend_from_slice(&face.quad_mesh_indices(positions.len() as u32));
positions.extend_from_slice(&face.quad_mesh_positions(&quad.into(), 1.0));
normals.extend_from_slice(&face.quad_mesh_normals());
}
}
let mut render_mesh = Mesh::new(PrimitiveTopology::TriangleList);
render_mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
VertexAttributeValues::Float32x3(positions),
);
render_mesh.insert_attribute(
Mesh::ATTRIBUTE_NORMAL,
VertexAttributeValues::Float32x3(normals),
);
render_mesh.insert_attribute(
Mesh::ATTRIBUTE_UV_0,
VertexAttributeValues::Float32x2(vec![[0.0; 2]; num_vertices]),
);
render_mesh.set_indices(Some(Indices::U32(indices.clone())));
meshes.add(render_mesh)
}
}

10
app/src/voxels/mesh.rs Normal file
View File

@ -0,0 +1,10 @@
use block_mesh::ilattice::glam;
use glam::Vec3A;
pub fn into_domain(_array_dim: u32, [x, y, z]: [u32; 3]) -> Vec3A {
Vec3A::new(x as f32, y as f32, z as f32) - 1.0
/*
let result = (2.0 / array_dim as f32) * Vec3A::new(x as f32, y as f32, z as f32) - 1.0;
println!("into_domain {},{},{} -> {}", x, y, z, result);
result*/
}

3
app/src/voxels/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod bool_voxel;
pub mod layers;
pub mod mesh;

4
app/wasm-build.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --out-dir ./web_out --target web ../target/wasm32-unknown-unknown/release/voxel-level-editor.wasm

28
common/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
block-mesh = { version = "0.2.0", optional = true }
ron = { version = "0.8", optional = true }
serde = { version = "1.0", optional = true }
strum = { version = "0.24", optional = true }
strum_macros = { version = "0.24", optional = true }
bevy = { version = "0.8", default-features = false, optional = true }
dot_vox = { version = "4.1.0", optional = true }
thiserror = { version = "1.0", optional = true }
[features]
# there are probably some invalid and untested combinations of these!
two_dimensional = []
three_dimensional = [ "block-mesh", "two_dimensional" ]
serde = ["dep:serde", "dep:ron", "two_dimensional", "three_dimensional", "std"]
bevy = ["dep:bevy"]
import_dot_vox = ["dep:dot_vox"]
block-mesh = ["dep:block-mesh"]
std = ["dep:strum", "dep:strum_macros", "dep:thiserror"]
fixed_arrays_instead_of_vecs = []

29
common/src/lib.rs Normal file
View File

@ -0,0 +1,29 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod space;
#[cfg(feature = "serde")]
pub use ron;
#[cfg(feature = "std")]
pub use strum;
#[cfg(feature = "std")]
pub use strum_macros;
pub mod gba_prelude {
pub use crate::space::level_gba::*;
pub use crate::space::level_tile::{
LevelTile,
LevelTile::*, // the generated code doesn't namespace the enum variants
Climbable,
};
pub use crate::space::two_dimensional::depth_tiles::{
DepthTile,
Direction
};
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}

View File

@ -0,0 +1,12 @@
use crate::gba_prelude::DepthTile;
use crate::gba_prelude::Direction;
use crate::gba_prelude::LevelTile;
pub struct ImportedMap {
pub tiles_north: &'static [DepthTile<LevelTile>],
pub tiles_south: &'static [DepthTile<LevelTile>],
pub tiles_east: &'static [DepthTile<LevelTile>],
pub tiles_west: &'static [DepthTile<LevelTile>],
pub player_start: [i32; 3],
pub start_direction: Direction,
}

View File

@ -0,0 +1,43 @@
use serde::{Serialize, Deserialize};
use thiserror::Error;
use crate::space::level_tile::LevelTile;
use crate::space::two_dimensional::depth_tiles::Direction;
use super::two_dimensional::depth_tiles::DepthTile;
#[cfg(feature = "three_dimensional")]
use super::three_dimensional::vec3generic::Vec3Generic;
#[cfg(all(feature = "three_dimensional"))]
pub fn create_export_map(voxels: &mut super::three_dimensional::hash_map_container::VoxelHashMapLayer<LevelTile>) -> Result<ExportedMap, ExportError> {
let project_voxels_dir = |d| super::three_dimensional::project_to_2d::create_tilemap(voxels, d, 32, 32, 32);
Ok(ExportedMap {
tiles_north: project_voxels_dir(Direction::North),
tiles_south: project_voxels_dir(Direction::South),
tiles_east: project_voxels_dir(Direction::East),
tiles_west: project_voxels_dir(Direction::West),
player_start: Vec3Generic { x: 0, y: 0, z: 0 },
start_direction: Direction::North,
})
}
#[cfg(all(feature = "serde", feature = "three_dimensional"))]
#[derive(Debug, Error)]
pub enum ExportError {
#[error("serialization failed")]
SerializationError(#[from] ron::Error)
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg(feature = "std")]
pub struct ExportedMap {
pub tiles_north: Vec<DepthTile<LevelTile>>,
pub tiles_south: Vec<DepthTile<LevelTile>>,
pub tiles_east: Vec<DepthTile<LevelTile>>,
pub tiles_west: Vec<DepthTile<LevelTile>>,
pub player_start: Vec3Generic<i32>,
pub start_direction: Direction,
}

View File

@ -0,0 +1,77 @@
#[cfg(not(feature = "std"))]
use core::default::Default;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum LevelTile {
#[default]
None,
Solid(Climbable),
SemisolidPlatform,
Door(DoorInfo),
EntitySpawn(EntityInfo),
}
#[derive(Copy, Clone, Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Climbable {
pub front: bool,
pub left: bool,
pub right: bool,
} // TODO: modular_bitfield?
#[derive(Copy, Clone, Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DoorInfo {
id: u8,
}
#[derive(Copy, Clone, Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EntityInfo {
id: u8,
}
#[cfg(feature = "block-mesh")]
pub mod block_mesh_impl {
use block_mesh::{Voxel, VoxelVisibility};
use super::LevelTile;
impl Voxel for LevelTile {
fn get_visibility(&self) -> VoxelVisibility {
get_visibility(self)
}
}
fn get_visibility(v: &LevelTile) -> VoxelVisibility {
match v {
LevelTile::None => VoxelVisibility::Empty,
LevelTile::Solid(_) => VoxelVisibility::Opaque,
LevelTile::SemisolidPlatform => VoxelVisibility::Translucent,
LevelTile::Door(_) => VoxelVisibility::Translucent,
LevelTile::EntitySpawn(_) => VoxelVisibility::Translucent,
}
}
}
pub const NULL_TILE: LevelTile = LevelTile::None;
const DEFAULT_NONE: LevelTile = LevelTile::None;
const DEFAULT_SOLID: LevelTile = LevelTile::Solid(Climbable {
front: false,
left: false,
right: false,
});
#[cfg(feature = "three_dimensional")]
impl super::three_dimensional::traits::DefaultVoxel for LevelTile {
fn get_default(kind: super::three_dimensional::traits::DefaultVoxelKinds) -> Self {
match kind {
super::three_dimensional::traits::DefaultVoxelKinds::None => DEFAULT_NONE,
super::three_dimensional::traits::DefaultVoxelKinds::Solid => DEFAULT_SOLID,
}
}
}

10
common/src/space/mod.rs Normal file
View File

@ -0,0 +1,10 @@
#[cfg(feature = "three_dimensional")]
pub mod three_dimensional;
#[cfg(feature = "two_dimensional")]
pub mod two_dimensional;
#[cfg(feature = "serde")]
pub mod level_serde;
pub mod level_tile;
pub mod level_gba;

View File

@ -0,0 +1,66 @@
use std::collections::HashMap;
use crate::space::three_dimensional::{
traits::{DefaultVoxelKinds, VoxelContainer},
vec3generic::Vec3Generic,
};
use block_mesh::Voxel;
#[cfg(feature = "import_dot_vox")]
use dot_vox::DotVoxData;
use super::traits::DefaultVoxel;
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "bevy", derive(bevy::prelude::Component))]
pub struct VoxelHashMapLayer<TVoxel> {
pub voxels: HashMap<Vec3Generic<i32>, TVoxel>,
pub size: Vec3Generic<i32>,
}
impl<TVoxel> VoxelContainer<TVoxel> for VoxelHashMapLayer<TVoxel>
where
TVoxel: Voxel + Copy,
{
fn get_voxel_at_pos<'a>(&self, pos: Vec3Generic<i32>) -> Option<TVoxel> {
self.voxels.get(&pos).copied()
}
}
#[cfg(feature = "import_dot_vox")]
pub fn import_from_bytes<T>(data: &[u8]) -> Result<VoxelHashMapLayer<T>, &'static str>
where
T: Voxel + DefaultVoxel + Default,
{
let v = dot_vox::load_bytes(data)?;
Ok(v.into())
}
#[cfg(feature = "import_dot_vox")]
impl<T> From<DotVoxData> for VoxelHashMapLayer<T>
where
T: Voxel + DefaultVoxel + Default,
{
fn from(d: DotVoxData) -> Self {
let mut w: VoxelHashMapLayer<T> = Default::default();
for model in d.models {
for voxel in model.voxels {
w.voxels.insert(
Vec3Generic {
x: voxel.x as i32,
y: voxel.z as i32,
z: voxel.y as i32,
},
DefaultVoxel::get_default(DefaultVoxelKinds::Solid),
);
}
w.size.x = w.size.x.max(model.size.x as i32);
w.size.y = w.size.y.max(model.size.z as i32);
w.size.z = w.size.z.max(model.size.y as i32);
}
println!("loaded world size {:?}", w.size);
w
}
}

View File

@ -0,0 +1,4 @@
pub mod hash_map_container;
pub mod project_to_2d;
pub mod traits;
pub mod vec3generic;

View File

@ -0,0 +1,139 @@
use block_mesh::Voxel;
use crate::space::two_dimensional::depth_tiles::{DepthTile, Direction};
use super::{traits::VoxelContainer, vec3generic::Vec3Generic};
#[derive(Debug)]
struct VoxelCursor<TVoxel> {
position: Vec3Generic<i32>,
finished: bool,
found: Option<TVoxel>,
}
impl<T> VoxelCursor<T> {
pub fn new(position: Vec3Generic<i32>) -> VoxelCursor<T> {
VoxelCursor::<T> {
position,
finished: false,
found: None,
}
}
}
pub fn create_tilemap<TContainer, TVoxel>(
container: &TContainer,
direction: Direction,
width: u32,
height: u32,
search_depth: u32,
) -> Vec<DepthTile<TVoxel>>
where
TContainer: VoxelContainer<TVoxel>,
TVoxel: Voxel + Copy + Default,
{
let step = match direction {
Direction::North => Vec3Generic::<i32> { x: 0, y: 0, z: 1 },
Direction::South => Vec3Generic::<i32> { x: 0, y: 0, z: -1 },
Direction::East => Vec3Generic::<i32> { x: 1, y: 0, z: 0 },
Direction::West => Vec3Generic::<i32> { x: -1, y: 0, z: 0 },
};
// create a bunch of cursors
let mut cursors: Vec<VoxelCursor<TVoxel>> = vec![];
for x in 0..width as i32 {
for y in 0..height as i32 {
cursors.push(match direction {
Direction::North => VoxelCursor::new(Vec3Generic::<i32> { x, y, z: 0 }),
Direction::South => VoxelCursor::new(Vec3Generic::<i32> {
x,
y,
z: search_depth as i32 - 1,
}),
Direction::East => VoxelCursor::new(Vec3Generic::<i32> { x: 0, y, z: x }),
Direction::West => VoxelCursor::new(Vec3Generic::<i32> {
x: search_depth as i32 - 1,
y,
z: x,
}),
});
}
}
// move them each until they bump into something.
// spec: https://www.youtube.com/watch?v=wwvLlEtxX3o
let mut num_finished_cursors = 0;
while num_finished_cursors < cursors.len() - 1 {
println!(
"num finished cursors: {}/{}",
num_finished_cursors,
cursors.len()
);
for cursor in &mut cursors {
if cursor.finished {
continue;
}
// is there a visible voxel at the current location? if so, we're done.
match container.get_voxel_at_pos(cursor.position) {
Some(v) => match v.get_visibility() {
block_mesh::VoxelVisibility::Opaque
| block_mesh::VoxelVisibility::Translucent => {
cursor.found = Some(v);
cursor.finished = true;
num_finished_cursors += 1;
continue;
}
block_mesh::VoxelVisibility::Empty => (),
},
None => (),
};
// step the cursor
cursor.position = cursor.position + step;
// test if the cursor is out of bounds (any dimension is outside of 0 <= n < search_depth)
for n in [&cursor.position.x, &cursor.position.y, &cursor.position.z] {
if *n < 0 || *n > search_depth as i32 {
cursor.finished = true;
num_finished_cursors += 1;
}
}
}
}
println!("all cursors finished.");
// todo: do we need to normalize the depth relative to camera? i.e. closest layer is depth 0
// even if it was on the backside
let mut tiles: Vec<DepthTile<TVoxel>> = vec![];
for cursor in cursors {
match cursor.found {
Some(v) => {
// determine the x, y, and depth based on the direction and the voxel's vector
let (x, y, depth) = match direction {
Direction::North => (cursor.position.x, cursor.position.y, cursor.position.z),
Direction::South => (
cursor.position.x,
cursor.position.y,
cursor.position.z,
), // flip x and z
Direction::East => (cursor.position.z, cursor.position.y, cursor.position.x),
Direction::West => (
cursor.position.z,
cursor.position.y,
cursor.position.x,
),
};
tiles.push(DepthTile::<TVoxel> {
x,
y,
depth,
data: v,
});
}
None => (),
}
}
tiles
}

View File

@ -0,0 +1,19 @@
use super::vec3generic::Vec3Generic;
/// A container for voxels that have 3-dimensional integer coordinates.
//#[cfg(feature = "block-mesh")]
pub trait VoxelContainer<TVoxel>
where
TVoxel: Copy,
{
fn get_voxel_at_pos(&self, pos: Vec3Generic<i32>) -> Option<TVoxel>;
}
pub trait DefaultVoxel {
fn get_default(kind: DefaultVoxelKinds) -> Self;
}
pub enum DefaultVoxelKinds {
None,
Solid,
}

View File

@ -0,0 +1,41 @@
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use std::ops::Add;
use block_mesh::ilattice::prelude::Vec3A;
#[derive(Eq, PartialEq, Hash, Debug, Default, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Vec3Generic<T> {
pub x: T,
pub y: T,
pub z: T,
}
impl From<Vec3A> for Vec3Generic<i32> {
fn from(v: Vec3A) -> Self {
Self {
x: (v.x.round()) as i32,
y: v.y.round() as i32,
z: v.z.round() as i32,
}
}
}
impl Into<[u32; 3]> for Vec3Generic<i32> {
fn into(self) -> [u32; 3] {
[self.x as u32, self.y as u32, self.z as u32]
}
}
impl Add<Self> for Vec3Generic<i32> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::Output {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
}
}
}

View File

@ -0,0 +1,57 @@
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "std")]
use strum_macros::EnumIter;
#[cfg(feature = "three_dimensional")]
use crate::space::three_dimensional::{traits::VoxelContainer, vec3generic::Vec3Generic};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "std", derive(EnumIter))]
pub enum Direction {
North,
South,
East,
West,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug)]
pub struct DepthTile<T>
where
T: Default,
{
pub data: T,
pub x: i32,
pub y: i32,
pub depth: i32,
}
#[cfg(feature = "std")]
#[derive(Default)]
#[cfg_attr(feature = "bevy", derive(bevy::prelude::Component))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DepthTileContainer<T>
where
T: Default,
{
pub tiles: Vec<DepthTile<T>>,
}
#[cfg(all(feature = "block-mesh", feature = "three_dimensional"))]
/// Implementation that projects depth tiles back into voxels from the north face, mostly for visualization in the editor
impl<T> VoxelContainer<T> for DepthTileContainer<T>
where
T: Copy + Default,
{
fn get_voxel_at_pos(&self, pos: Vec3Generic<i32>) -> Option<T> {
for tile in &self.tiles {
if tile.x == pos.x && tile.y == pos.y && tile.depth == pos.z {
return Some(tile.data);
}
}
None
}
}

View File

@ -0,0 +1 @@
pub mod depth_tiles;

93
flake.lock Normal file
View File

@ -0,0 +1,93 @@
{
"nodes": {
"nixGL": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1656321324,
"narHash": "sha256-Sz0uWspqvshGFbT+XmRVVayuW514rNNLLvrre8jBLLU=",
"owner": "guibou",
"repo": "nixGL",
"rev": "047a34b2f087e2e3f93d43df8e67ada40bf70e5c",
"type": "github"
},
"original": {
"owner": "guibou",
"repo": "nixGL",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1659868656,
"narHash": "sha256-LINDS957FYzOb412t/Zha44LQqGniMpUIUz4Pi+fvSs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "80fc83ad314fe701766ee66ac8286307d65b39e3",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs-mozilla": {
"flake": false,
"locked": {
"lastModified": 1657214286,
"narHash": "sha256-rO/4oymKXU09wG2bcTt4uthPCp1XsBZjxuCJo3yVXNs=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "0508a66e28a5792fdfb126bbf4dec1029c2509e0",
"type": "github"
},
"original": {
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1659868656,
"narHash": "sha256-LINDS957FYzOb412t/Zha44LQqGniMpUIUz4Pi+fvSs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "80fc83ad314fe701766ee66ac8286307d65b39e3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixGL": "nixGL",
"nixpkgs": "nixpkgs_2",
"nixpkgs-mozilla": "nixpkgs-mozilla",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

99
flake.nix Normal file
View File

@ -0,0 +1,99 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
nixGL.url = "github:guibou/nixGL";
nixpkgs-mozilla = { url = "github:mozilla/nixpkgs-mozilla"; };
};
outputs = { self, nixpkgs, utils, nixpkgs-mozilla, nixGL }:
utils.lib.eachDefaultSystem (system:
let
pkgs = (import nixpkgs) {
inherit system;
overlays = [ (import nixpkgs-mozilla) nixGL.overlay ];
};
toolchain = (pkgs.rustChannelOf {
# sadly, https://github.com/mozilla/nixpkgs-mozilla/issues/287
date = "2022-08-07";
channel = "nightly";
sha256 = "sha256-cXABXvQuh4dXNLtFMvRuB7YBi9LXRerA0u/u/TUm4rQ=";
}).rust.override {
extensions = [
"rust-src"
"rust-analyzer-preview"
"clippy-preview"
"rustfmt-preview"
];
targets = [ "x86_64-unknown-linux-gnu" "wasm32-unknown-unknown" ];
};
nixGlWrappedMgbaQt = pkgs.writeShellScriptBin "mgba-qt" ''
# call mgba-qt with nixGL
exec ${pkgs.nixgl.nixGLIntel}/bin/nixGLIntel ${pkgs.mgba}/bin/mgba-qt $@
'';
# this doesn't seem to work
softwareVulkan = pkgs.writeShellScriptBin "hell" ''
export LIBGL_ALWAYS_SOFTWARE=1
export __GLX_VENDOR_LIBRARY_NAME=mesa
export VK_ICD_FILENAMES=${pkgs.mesa.drivers}/share/vulkan/icd.d/lvp_icd.x86_64.json
exec ${pkgs.nixgl.nixVulkanIntel}/bin/nixVulkanIntel $@
'';
baseBuildInputs = [ toolchain pkgs.gcc-arm-embedded ];
levelEditorDeps = with pkgs; {
# thanks, https://github.com/bevyengine/bevy/blob/main/docs/linux_dependencies.md#nixos !
nativeBuildInputs = [ pkgconfig llvmPackages.bintools vulkan-loader nixgl.nixVulkanIntel vulkan-tools
openssl # deps for building wasm. we have to cargo install -f wasm-bindgen-cli, the nixpkg is out of sync
];
buildInputs = [ # wip and not minimal. was trying to get stuff working on my desktop and didn't finish
udev alsaLib
xlibsWrapper xorg.libXcursor xorg.libXrandr xorg.libXi # To use x11 feature
libxkbcommon wayland # To use wayland feature
gdk-pixbuf atk pango cairo gtk3-x11 # additional dependencies for voxel-level-editor
];
hook = ''
export PATH=$PATH:$HOME/.cargo/bin
'';
};
ensureSubmodules = ''
# quick check of whether submodules are initialized, if the working directory is the root of the repo
if [[ -f "flake.nix" && -f "Cargo.toml" && ! -f "external/agb/README.md" ]]; then
echo "fetching submodules"
git submodule init
git submodule update
fi
'';
in {
devShell = with pkgs;
mkShell {
nativeBuildInputs = baseBuildInputs ++ [ nixGlWrappedMgbaQt ];
shellHook = ''
'' + ensureSubmodules;
};
devShells.level-editor = with pkgs; # wip use at your own peril
mkShell rec {
nativeBuildInputs = baseBuildInputs ++ levelEditorDeps.nativeBuildInputs ++ [ nixGlWrappedMgbaQt softwareVulkan ];
buildInputs = levelEditorDeps.buildInputs;
shellHook = ''
'' + ensureSubmodules;
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
};
devShells.wsl-vcxsrv = with pkgs;
mkShell {
nativeBuildInputs = baseBuildInputs ++ [ nixGlWrappedMgbaQt ];
shellHook = ''
export HOST_IP="$(ip route |awk '/^default/{print $3}')"
export DISPLAY=$HOST_IP:0 # use vcxsrv
export PULSE_SERVER="tcp:$HOST_IP"
'' + ensureSubmodules;
};
});
}

3
rust-toolchain.toml Normal file
View File

@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["rust-src", "clippy"]

7
shell.nix Normal file
View File

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).shellNix