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