Skip to main content

rscad/
lib.rs

1#[allow(ambiguous_glob_reexports)]
2pub use rscad_core::*;
3#[cfg(feature = "export")]
4pub use rscad_openscad::export::ExportObject;
5#[cfg(feature = "import")]
6pub use rscad_openscad::import::import;
7
8use bevy::prelude::*;
9use rscad_engine::platform::{Platform, PlatformInit, PlatformWatcher, PlatformWindow};
10use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};
11
12/// HTML-chrome (Leptos) browser startup; the bevy_ui web lanes use
13/// [`main`] instead.
14#[cfg(all(feature = "leptos", target_arch = "wasm32"))]
15pub mod web_html;
16
17/// Terminal log layer with env-driven filtering (native builds only; on wasm
18/// its default timer would call `SystemTime::now()`, which panics).
19#[cfg(not(target_arch = "wasm32"))]
20fn fmt_layer<S>() -> impl Layer<S>
21where
22    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
23{
24    tracing_subscriber::fmt::layer().with_filter({
25        let default_directive = "info".parse().expect("BUG: invalid default directive");
26        let builder = tracing_subscriber::filter::EnvFilter::builder()
27            .with_default_directive(default_directive);
28
29        // RSCAD_LOG takes priority, then RUST_LOG, then the default.
30        if std::env::var("RSCAD_LOG").is_ok() {
31            builder
32                .with_env_var("RSCAD_LOG")
33                .from_env()
34                .unwrap_or_default()
35        } else {
36            builder
37                .with_env_var("RUST_LOG")
38                .from_env()
39                .unwrap_or_default()
40        }
41    })
42}
43
44/// Log filter for wasm builds. Env vars aren't available in the browser, so the
45/// directives are baked in: a sensible default level, but the very chatty GPU
46/// backends (wgpu/naga) are knocked down to `warn` so they don't flood the
47/// browser console and the in-app log panel.
48#[cfg(target_arch = "wasm32")]
49fn wasm_log_filter() -> tracing_subscriber::EnvFilter {
50    tracing_subscriber::EnvFilter::new("info,wgpu=warn,wgpu_core=warn,wgpu_hal=warn,naga=warn")
51}
52
53/// Browser (wasm) entry point: wasm-bindgen's bootstrap (generated by trunk
54/// from the crate's `cdylib` artifact) runs this once the module is
55/// instantiated. The chrome is picked by the compiled features — the
56/// entrypoint HTML's `data-cargo-features` selects the lane, so both can
57/// never be enabled by a bundle build.
58#[cfg(target_arch = "wasm32")]
59#[wasm_bindgen::prelude::wasm_bindgen(start)]
60fn wasm_main() {
61    #[cfg(feature = "leptos")]
62    web_html::main();
63    #[cfg(all(feature = "gui", not(feature = "leptos")))]
64    main();
65}
66
67#[bevy_main]
68pub fn main() {
69    // Surface Rust panics in the browser devtools console (the bevy_ui web
70    // lane enters here from `wasm_main`).
71    #[cfg(target_arch = "wasm32")]
72    console_error_panic_hook::set_once();
73
74    // Set up tracing. With the GUI, also tee logs into the in-app log panel.
75    // On wasm, route output to the browser console (tracing-wasm) instead of
76    // the terminal fmt layer, whose default timer panics on wasm.
77    #[cfg(feature = "gui")]
78    let log_rx = {
79        let (log_tx, log_rx) = flume::unbounded();
80        let panel = rscad_gui::UiLogLayer { tx: log_tx };
81
82        // Gate the panel at INFO: debug/trace stay terminal-only (RSCAD_LOG).
83        #[cfg(not(target_arch = "wasm32"))]
84        tracing_subscriber::registry()
85            .with(panel.with_filter(tracing_subscriber::filter::LevelFilter::INFO))
86            .with(fmt_layer())
87            .init();
88        // On wasm, route to the browser console; both sinks share a filter that
89        // silences the very chatty GPU backends so they don't flood the panel.
90        #[cfg(target_arch = "wasm32")]
91        tracing_subscriber::registry()
92            .with(panel.with_filter(wasm_log_filter()))
93            .with(tracing_wasm::WASMLayer::new(Default::default()).with_filter(wasm_log_filter()))
94            .init();
95
96        log_rx
97    };
98    #[cfg(all(not(feature = "gui"), not(target_arch = "wasm32")))]
99    tracing_subscriber::registry().with(fmt_layer()).init();
100    #[cfg(all(not(feature = "gui"), target_arch = "wasm32"))]
101    tracing_subscriber::registry()
102        .with(tracing_wasm::WASMLayer::new(Default::default()).with_filter(wasm_log_filter()))
103        .init();
104
105    let platform = rscad_engine::platform::current_platform();
106    tracing::info!("Platform: {}", platform.name());
107
108    // ── Initial model loading ────────────────────────────────────
109    // Without a CLI path the app boots empty and lands on the projects
110    // screen; the placeholder source has no real file to watch.
111    let cli_open = std::env::args().nth(1).is_some();
112    let initial = platform.load_initial_model();
113    let watcher = cli_open
114        .then(|| platform.create_watcher(&initial.model_state.source))
115        .flatten();
116
117    // ── App ──────────────────────────────────────────────────────
118    let mut app = App::new();
119    if let Some(watcher) = watcher {
120        app.insert_resource(watcher);
121    }
122    app.insert_resource(platform.winit_settings())
123        .add_plugins(
124            DefaultPlugins
125                .build()
126                .disable::<bevy::log::LogPlugin>()
127                .set(platform.window_plugin()),
128        )
129        // Pre-insert the platform (already constructed for the file watcher);
130        // the engine's PlatformPlugin keeps it rather than making a new one.
131        .insert_resource(rscad_engine::platform::PlatformRes(platform))
132        .add_plugins(rscad_engine::EnginePlugins {
133            init: rscad_engine::ModelInit {
134                source: initial.model_state.source,
135                scene: initial.model_state.scene,
136                pre_built_mesh: initial.pre_built_mesh,
137            },
138            // The GUI spawns its own interactive camera; the engine only
139            // spawns one for GUI-less (canvas-only) builds.
140            spawn_camera: cfg!(not(feature = "gui")),
141        });
142
143    // GUI: toolbar, panels, editor, keybinds, and the log panel.
144    #[cfg(feature = "gui")]
145    {
146        // Land on the projects screen unless a file was opened directly.
147        app.insert_resource(rscad_gui::ProjectsOpen(!cli_open));
148        app.add_plugins(rscad_gui::GuiPlugin);
149        app.insert_resource(rscad_gui::Logs {
150            rx: log_rx,
151            entries: Vec::new(),
152            rendered_count: 0,
153        });
154    }
155
156    app.run();
157}