use bevy::dev_tools::infinite_grid::{InfiniteGrid, InfiniteGridPlugin, InfiniteGridSettings};
use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
use bevy::prelude::*;
use std::path::PathBuf;
use box_select::BoxSelectPlugin;
use camera::{CameraPlugin, OrbitCamera, fit_bounds, radius_limits};
use fem_core::{FemModel, MainViewportCamera};
use gmsh;
use interaction::InteractionPlugin;
use picking::PickingPlugin;
use selection::SelectionPlugin;
use ui::UiPlugin;
use visualization::VisualizationPlugin;
mod icon;
fn main() {
let arg_path = std::env::args_os().nth(1).map(PathBuf::from);
let mut initial_setup = fem_core::AnalysisSetup::default();
let mut setup_has_content = false;
let model = arg_path.and_then(|path| {
let (mesh_path, cnt_path) = match hecmw::load_hecmw_ctrl(&path) {
Ok(content) if content.mesh_path.is_some() => hecmw::resolve_paths(&path, &content),
_ => (Some(path), None),
};
let (model, materials, sections) = load_initial_model(mesh_path?)?;
for m in materials {
initial_setup.materials.push(m);
setup_has_content = true;
}
for s in sections {
initial_setup.sections.push(s);
setup_has_content = true;
}
if let Some(cnt_path) = cnt_path {
match hecmw::load_cnt_file(&cnt_path, &model.meshes[0], 0) {
Ok(data) => {
data.merge_into(&mut initial_setup);
setup_has_content = true;
}
Err(e) => eprintln!("Failed to parse {}: {e}", cnt_path.display()),
}
}
Some(model)
});
let mut app = App::new();
if let Some(model) = model {
app.insert_resource(model);
if setup_has_content {
app.insert_resource(initial_setup);
}
}
app.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.025)))
.insert_resource(GlobalAmbientLight {
color: Color::srgb(0.82, 0.88, 0.95),
brightness: 350.0,
..default()
})
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: format!("bevyistr v{} — FrontISTR Pre/Post", env!("CARGO_PKG_VERSION")),
..default()
}),
..default()
}))
.add_plugins(WireframePlugin::default())
.add_systems(Update, icon::set_window_icon)
.insert_resource(WireframeConfig {
global: false,
default_color: Color::srgb(0.55, 0.82, 0.95),
..default()
})
.add_plugins((
CameraPlugin,
InteractionPlugin,
PickingPlugin,
SelectionPlugin,
VisualizationPlugin,
UiPlugin,
BoxSelectPlugin,
))
.add_plugins(FrameTimeDiagnosticsPlugin { ..default() })
.add_plugins(InfiniteGridPlugin)
.add_systems(Startup, (setup, spawn_grid))
.add_systems(
Update,
(
rescale_grid_on_reload,
update_grid_visibility.after(interaction::InteractionSystems::Navigation),
),
)
.run();
}
fn load_initial_model(
path: PathBuf,
) -> Option<(FemModel, Vec<fem_core::FemMaterial>, Vec<fem_core::Section>)> {
match hecmw::load_mesh_file_with_setup(&path) {
Ok((mesh, materials, sections)) => {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("mesh")
.to_string();
Some((FemModel::single_mesh(name, mesh), materials, sections))
}
Err(e) => {
match gmsh::load_msh_file(&path) {
Ok(mesh) => {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("mesh")
.to_string();
Some((FemModel::single_mesh(name, mesh), Vec::new(), Vec::new()))
}
Err(_) => {
eprintln!("Failed to load {}: {e}", path.display());
None
}
}
}
}
}
fn setup(mut commands: Commands, model: Option<Res<FemModel>>) {
let (focus, radius) = model
.as_deref()
.and_then(FemModel::bounds)
.map(|(min, max)| fit_bounds(min, max))
.unwrap_or((Vec3::ZERO, 10.0));
let (min_radius, max_radius) = radius_limits(radius);
let camera_position = focus + Vec3::new(radius * 0.45, radius * 0.45, radius);
commands.spawn((
Camera3d::default(),
Camera::default(),
IsDefaultUiCamera,
MainViewportCamera,
Transform::from_translation(camera_position).looking_at(focus, Vec3::Y),
OrbitCamera {
focus,
target_focus: focus,
radius,
min_radius,
max_radius,
},
));
commands.spawn((
DirectionalLight {
color: Color::srgb(1.0, 0.96, 0.90),
illuminance: 3_200.0,
shadow_maps_enabled: false,
..default()
},
Transform::from_xyz(6.0, 10.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
DirectionalLight {
color: Color::srgb(0.78, 0.86, 0.95),
illuminance: 1_000.0,
shadow_maps_enabled: false,
..default()
},
Transform::from_xyz(-5.0, -3.0, -5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn spawn_grid(mut commands: Commands) {
commands.spawn((
InfiniteGrid,
Visibility::Visible,
InfiniteGridSettings {
x_axis_color: Color::srgb(0.70, 0.20, 0.20),
z_axis_color: Color::srgb(0.20, 0.50, 0.70),
major_line_color: Color::srgba(0.35, 0.50, 0.60, 0.55),
minor_line_color: Color::srgba(0.20, 0.32, 0.40, 0.30),
..default()
},
));
}
fn update_grid_visibility(
camera: Query<(&Transform, &OrbitCamera), With<MainViewportCamera>>,
mut grids: Query<&mut Visibility, With<InfiniteGrid>>,
) {
let Ok((transform, orbit)) = camera.single() else {
return;
};
let visibility = grid_visibility_for_direction(transform.translation - orbit.focus);
for mut current in &mut grids {
if *current != visibility {
*current = visibility;
}
}
}
fn grid_visibility_for_direction(direction_from_focus: Vec3) -> Visibility {
const EDGE_ON_NORMAL_COMPONENT: f32 = 0.02;
let normal_component =
if direction_from_focus.is_finite() && direction_from_focus.length_squared() > 1.0e-12 {
direction_from_focus.normalize().dot(Vec3::Y).abs()
} else {
0.0
};
if normal_component <= EDGE_ON_NORMAL_COMPONENT {
Visibility::Hidden
} else {
Visibility::Visible
}
}
fn rescale_grid_on_reload(
model: Option<Res<FemModel>>,
version: Res<fem_core::FemModelVersion>,
mut last_version: Local<Option<u64>>,
mut grid_query: Query<&mut InfiniteGridSettings, With<InfiniteGrid>>,
) {
let current = version.value;
if *last_version == Some(current) {
return;
}
*last_version = Some(current);
let Some((min, max)) = model.as_deref().and_then(FemModel::bounds) else {
return;
};
let (_, radius) = fit_bounds(min, max);
let Ok(mut settings) = grid_query.single_mut() else {
return;
};
const REFERENCE_RADIUS: f32 = 3.3;
settings.fadeout_distance = (radius * 6.0).max(100.0);
settings.scale = (REFERENCE_RADIUS / radius).clamp(1.0e-4, 10.0);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_is_hidden_for_axis_views_parallel_to_its_plane() {
for direction in [Vec3::X, -Vec3::X, Vec3::Z, -Vec3::Z] {
assert_eq!(grid_visibility_for_direction(direction), Visibility::Hidden);
}
}
#[test]
fn grid_remains_visible_from_top_bottom_and_isometric_views() {
for direction in [
Vec3::Y,
-Vec3::Y,
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(-1.0, 1.0, -1.0),
] {
assert_eq!(
grid_visibility_for_direction(direction),
Visibility::Visible
);
}
}
}