initial commit
This commit is contained in:
commit
ebec2b8711
|
@ -0,0 +1,4 @@
|
||||||
|
target
|
||||||
|
.idea/*
|
||||||
|
!/.idea/runConfigurations
|
||||||
|
.DS_Store
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
members = [
|
||||||
|
"app",
|
||||||
|
"common",
|
||||||
|
]
|
||||||
|
|
||||||
|
resolver = "2" # need to use resolver = "2" to be able to build wgpu-hal
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
|
@ -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}"
|
||||||
|
}]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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?
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod cursor_layer;
|
||||||
|
pub mod fn_layer;
|
||||||
|
pub mod mutable_mesh;
|
||||||
|
pub mod named;
|
||||||
|
pub mod property_pane;
|
|
@ -0,0 +1,7 @@
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct MutableMesh {
|
||||||
|
pub current: bool,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Named {
|
||||||
|
pub name: String,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod layer_spawner;
|
||||||
|
pub mod mutable_mesh_refresher;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod generic_command_queue;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} */
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod layers;
|
||||||
|
pub mod properties;
|
||||||
|
pub mod util;
|
||||||
|
pub mod syntax_highlighting;
|
||||||
|
pub mod ui_spawner;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>>
|
||||||
|
}
|
|
@ -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:"));
|
||||||
|
});
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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*/
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod bool_voxel;
|
||||||
|
pub mod layers;
|
||||||
|
pub mod mesh;
|
|
@ -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
|
||||||
|
|
|
@ -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 = []
|
|
@ -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() {}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod hash_map_container;
|
||||||
|
pub mod project_to_2d;
|
||||||
|
pub mod traits;
|
||||||
|
pub mod vec3generic;
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod depth_tiles;
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
components = ["rust-src", "clippy"]
|
Loading…
Reference in New Issue