//! # sylpheed-viewer //! //! Bevy-based asset viewer for Project Sylpheed: Arc of Deception. //! //! This crate serves as both: //! - A **native binary** (via `main.rs`) — full desktop viewer //! - A **WASM library** (this file) — browser viewer at `index.html` //! //! ## Architecture //! - `sylpheed_formats` handles all binary parsing — no Bevy dependency //! - This crate is the thin Bevy integration layer on top //! - `asset_loader` bridges the two: wraps parsers as `AssetLoader` impls //! //! ## WASM notes //! On the web, local file access is unavailable. Assets must be //! pre-extracted and served over HTTP. XISO reading is native-only. use bevy::prelude::*; use bevy_egui::EguiPlugin; pub mod asset_loader; pub mod camera; pub mod iso_loader; pub mod ui; // ── Application state ──────────────────────────────────────────────────────── /// Global viewer state, shared across all UI and rendering systems. #[derive(Resource)] pub struct ViewerState { /// Whether the plain-text viewer wraps long lines. pub text_wrap: bool, } impl Default for ViewerState { fn default() -> Self { Self { text_wrap: true } } } // ── App builder ────────────────────────────────────────────────────────────── /// Build and run the viewer application. /// /// Called from `main.rs` on native and from `wasm_bindgen` init on the web. pub fn run() { let mut app = App::new(); app.add_plugins( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(), resolution: (1280.0, 720.0).into(), ..default() }), ..default() }), ); app.add_plugins(EguiPlugin); app.add_plugins(asset_loader::SylpheedAssetPlugin); app.add_plugins(iso_loader::IsoLoaderPlugin); app.add_plugins(camera::OrbitCameraPlugin); app.add_plugins(ui::ViewerUiPlugin); app.init_resource::(); // Minimal 3D scene: a directional light so meshes are visible app.add_systems(Startup, setup_scene); app.run(); } fn setup_scene(mut commands: Commands) { // Bright ambient so surfaces facing away from every light aren't pure black. commands.insert_resource(AmbientLight { color: Color::srgb(0.9, 0.93, 1.0), brightness: 900.0, }); // A multi-directional rig (key + fills + rim + underside) so an orbiting // camera always has some light on the visible side — the single front light // left the backside unreadable. let lights = [ (Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right (Vec3::new(-2.0, 1.0, 0.5), 4_500.0, false), // fill: left (Vec3::new(0.5, 0.8, -2.0), 6_000.0, false), // rim: behind (Vec3::new(0.0, -1.5, 0.5), 2_500.0, false), // underside fill ]; for (from, lux, shadows) in lights { commands.spawn(( DirectionalLight { illuminance: lux, shadows_enabled: shadows, ..default() }, Transform::from_translation(from).looking_at(Vec3::ZERO, Vec3::Y), )); } }