Files
Sylpheed/crates/sylpheed-viewer/src/lib.rs
Fabian Hamm b2bc70d483 fix(wasm): select the bin target, so trunk emits a real bundle
`trunk build --release` reached the asset pipeline for the first time and
failed there:

  found more than one target artifact: ["sylpheed_viewer", "sylpheed-viewer"]

The crate declares both a [[bin]] `sylpheed-viewer` (src/main.rs) and a
[lib] `sylpheed_viewer` cdylib (src/lib.rs), and the index.html link named
neither, so trunk refused to guess.

Trunk's error offers two ways out and THEY ARE NOT EQUIVALENT. Measured,
both exiting 0:

  data-target-name="sylpheed_viewer"   1_478 bytes,        1 app symbol
  data-bin="sylpheed-viewer"          21_298_268 bytes, 2_998 app symbols

Selecting the lib "succeeds" while linking nothing, because there is no
wasm entry point in it -- no wasm-bindgen dependency, no import, no
`#[wasm_bindgen(start)]`. The linker drops the whole app and trunk emits an
empty module. That would have turned this job GREEN on a bundle that cannot
start, which is worse than the red it replaced.

`main()` is a valid wasm entry: it calls `sylpheed_viewer::run()` and its
only native-specific code is already `#[cfg(not(target_arch = "wasm32"))]`.
With the bin selected, trunk injects a real init -- `import init`, an
integrity-checked module preload, `__wbindgen_start`, and the
`TrunkApplicationStarted` event.

The lib.rs docs claimed this file was the WASM entry point "called from
`wasm_bindgen` init on the web". Nothing ever called it. That comment is
what made the lib look like the right target, so it is corrected here
rather than left to mislead the next reader.

Verified locally with trunk 0.21.7 on x86_64. The exit code does not
distinguish these two cases -- only the artifact does.

Refs #11
2026-09-08 18:55:05 +02:00

104 lines
3.7 KiB
Rust

//! # 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 app**, also entered through `main.rs` — browser viewer at
//! `index.html`. Trunk builds the *bin* target (`data-bin` in index.html) and
//! wasm-bindgen calls `main()`; there is no `#[wasm_bindgen(start)]` here and
//! this file is NOT the wasm entry point. Selecting the lib target instead
//! links an empty 1.4 KB module that still exits 0 — see #11.
//!
//! ## 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 both native and wasm.
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_f32, 720.0_f32).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::<ViewerState>();
// 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),
));
}
}