@@ -26,11 +26,20 @@ use cu_bevymon::{CuBevyMonFocus, CuBevyMonSurface};
2626#[ cfg( not( target_arch = "wasm32" ) ) ]
2727use std:: path:: { Path , PathBuf } ;
2828#[ cfg( not( target_arch = "wasm32" ) ) ]
29- use std:: { fs, io} ;
29+ use std:: {
30+ fs,
31+ io:: { self , Write } ,
32+ } ;
3033
3134pub const BALANCEBOT : & str = "balancebot.glb" ;
3235pub const SKYBOX : & str = "skybox.ktx2" ;
3336pub const DIFFUSE_MAP : & str = "diffuse_map.ktx2" ;
37+ #[ cfg( not( target_arch = "wasm32" ) ) ]
38+ const BASE_ASSETS_URL : & str = "https://cdn.copper-robotics.com/" ;
39+ #[ cfg( not( target_arch = "wasm32" ) ) ]
40+ const SCENE_ASSETS : [ & str ; 3 ] = [ BALANCEBOT , SKYBOX , DIFFUSE_MAP ] ;
41+ #[ cfg( not( target_arch = "wasm32" ) ) ]
42+ const SCENE_ASSET_CACHE_DIR : & str = ".download-cache" ;
3443
3544const TABLE_HEIGHT : f32 = 0.724 ;
3645const RAIL_WIDTH : f32 = 0.55 ; // 55cm
@@ -103,6 +112,13 @@ struct WorldLayout {
103112 split_monitor : bool ,
104113}
105114
115+ #[ derive( Resource , Clone ) ]
116+ struct SceneAssetPaths {
117+ balance_bot : String ,
118+ skybox : String ,
119+ diffuse_map : String ,
120+ }
121+
106122#[ derive( SystemParam ) ]
107123struct SimInput < ' w > {
108124 layout : Res < ' w , WorldLayout > ,
@@ -201,6 +217,7 @@ pub fn build_world(app: &mut App, headless: bool, split_monitor: bool) -> &mut A
201217 . init_resource :: < SceneLoadState > ( )
202218 . insert_resource ( Gravity :: default ( ) )
203219 . insert_resource ( Time :: < Physics > :: default ( ) )
220+ . insert_resource ( prepare_scene_assets ( ) )
204221 . add_systems ( Startup , setup_scene)
205222 . add_systems ( Startup , setup_ui)
206223 . add_systems ( Update , setup_entities) // Wait for the cart entity to be loaded
@@ -292,142 +309,191 @@ fn ground_setup(
292309}
293310
294311#[ cfg( not( target_arch = "wasm32" ) ) ]
295- fn create_symlink ( src : & str , dst : & str ) -> io:: Result < ( ) > {
296- let dst_path = Path :: new ( dst) ;
312+ fn scene_asset_root ( ) -> PathBuf {
313+ PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( "assets" )
314+ }
297315
298- if dst_path. exists ( ) {
299- fs:: remove_file ( dst_path) ?;
316+ #[ cfg( not( target_arch = "wasm32" ) ) ]
317+ fn scene_asset_cache_root ( ) -> PathBuf {
318+ scene_asset_root ( ) . join ( SCENE_ASSET_CACHE_DIR )
319+ }
320+
321+ #[ cfg( not( target_arch = "wasm32" ) ) ]
322+ fn path_string ( path : & Path ) -> String {
323+ path. to_string_lossy ( ) . into_owned ( )
324+ }
325+
326+ #[ cfg( not( target_arch = "wasm32" ) ) ]
327+ fn asset_progress_bar ( done : usize , total : usize ) -> String {
328+ const WIDTH : usize = 28 ;
329+ let filled = WIDTH * done / total. max ( 1 ) ;
330+ let empty = WIDTH . saturating_sub ( filled) ;
331+ format ! (
332+ "[{}{}] {done}/{total}" ,
333+ "=" . repeat( filled) ,
334+ " " . repeat( empty)
335+ )
336+ }
337+
338+ #[ cfg( not( target_arch = "wasm32" ) ) ]
339+ fn link_or_copy_cached_asset ( src : & Path , dst : & Path ) -> io:: Result < ( ) > {
340+ if fs:: symlink_metadata ( dst) . is_ok ( ) {
341+ fs:: remove_file ( dst) ?;
300342 }
301343
302344 #[ cfg( unix) ]
303345 {
304- std:: os:: unix:: fs:: symlink ( src, dst)
346+ match std:: os:: unix:: fs:: symlink ( src, dst) {
347+ Ok ( ( ) ) => Ok ( ( ) ) ,
348+ Err ( symlink_err) => fs:: copy ( src, dst) . map ( |_| ( ) ) . map_err ( |copy_err| {
349+ io:: Error :: new (
350+ copy_err. kind ( ) ,
351+ format ! ( "failed to symlink ({symlink_err}) or copy ({copy_err})" ) ,
352+ )
353+ } ) ,
354+ }
305355 }
306356
307357 #[ cfg( windows) ]
308358 {
309- std:: os:: windows:: fs:: symlink_file ( src, dst)
359+ match std:: os:: windows:: fs:: symlink_file ( src, dst) {
360+ Ok ( ( ) ) => Ok ( ( ) ) ,
361+ Err ( symlink_err) => fs:: copy ( src, dst) . map ( |_| ( ) ) . map_err ( |copy_err| {
362+ io:: Error :: new (
363+ copy_err. kind ( ) ,
364+ format ! ( "failed to symlink ({symlink_err}) or copy ({copy_err})" ) ,
365+ )
366+ } ) ,
367+ }
310368 }
311369}
312370
313- /// Tries to get the asset path using the online cache first.
314- /// If that fails due to a network error, falls back to the offline cache.
315371#[ cfg( not( target_arch = "wasm32" ) ) ]
316372fn get_asset_path (
317373 online_cache : & Cache ,
318374 offline_cache : & Cache ,
319375 asset_url : & str ,
320- asset_name : & str , // For logging purposes
376+ asset_name : & str ,
321377) -> Result < PathBuf , CacheError > {
322- match online_cache . cached_path ( asset_url) {
378+ match offline_cache . cached_path ( asset_url) {
323379 Ok ( path) => Ok ( path) ,
324380 Err ( err) => {
325- // Check if the error is network-related
326381 if matches ! (
327382 err,
328- CacheError :: HttpError ( _) | CacheError :: IoError ( _ ) | CacheError :: ResourceNotFound ( _)
383+ CacheError :: NoCachedVersions ( _) | CacheError :: CacheCorrupted ( _)
329384 ) {
330- warn ! (
331- "Failed to fetch latest '{}' from network ({}). Attempting to use cached version." ,
332- asset_name, err
333- ) ;
334- // Fallback to offline cache
335- offline_cache. cached_path ( asset_url)
385+ eprintln ! ( " {asset_name}: cache miss; downloading from {asset_url}" ) ;
386+ online_cache. cached_path ( asset_url)
336387 } else {
337- // Not a network error, propagate the original error
338388 Err ( err)
339389 }
340390 }
341391 }
342392}
343393
344394#[ cfg( not( target_arch = "wasm32" ) ) ]
345- pub const BASE_ASSETS_URL : & str = "https://cdn.copper-robotics.com/" ;
395+ fn precached_asset_path (
396+ online_cache : & Cache ,
397+ offline_cache : & Cache ,
398+ asset_root : & Path ,
399+ index : usize ,
400+ total : usize ,
401+ asset_name : & str ,
402+ ) -> Result < String , CacheError > {
403+ let plain_path = asset_root. join ( asset_name) ;
404+ if plain_path. is_file ( ) {
405+ eprintln ! (
406+ " {} {asset_name}: cached" ,
407+ asset_progress_bar( index, total)
408+ ) ;
409+ return Ok ( path_string ( & plain_path) ) ;
410+ }
411+
412+ if fs:: symlink_metadata ( & plain_path) . is_ok ( ) {
413+ fs:: remove_file ( & plain_path) ?;
414+ }
415+
416+ eprintln ! (
417+ " {} {asset_name}: resolving" ,
418+ asset_progress_bar( index. saturating_sub( 1 ) , total)
419+ ) ;
420+ let asset_url = format ! ( "{BASE_ASSETS_URL}{asset_name}" ) ;
421+ let hashed_path = get_asset_path ( online_cache, offline_cache, & asset_url, asset_name) ?;
422+ link_or_copy_cached_asset ( & hashed_path, & plain_path) ?;
423+ eprintln ! ( " {} {asset_name}: ready" , asset_progress_bar( index, total) ) ;
424+ Ok ( path_string ( & plain_path) )
425+ }
426+
427+ #[ cfg( not( target_arch = "wasm32" ) ) ]
428+ fn prepare_scene_assets ( ) -> SceneAssetPaths {
429+ let asset_root = scene_asset_root ( ) ;
430+ let cache_root = scene_asset_cache_root ( ) ;
431+ fs:: create_dir_all ( & asset_root) . expect ( "failed to create scene asset directory" ) ;
432+
433+ eprintln ! (
434+ "Preparing Copper balancebot scene assets in {}" ,
435+ asset_root. display( )
436+ ) ;
437+ let _ = io:: stderr ( ) . flush ( ) ;
346438
347- fn setup_scene (
348- mut commands : Commands ,
349- asset_server : Res < AssetServer > ,
350- mut images : ResMut < Assets < Image > > ,
351- mut meshes : ResMut < Assets < Mesh > > ,
352- mut materials : ResMut < Assets < StandardMaterial > > ,
353- layout : Res < WorldLayout > ,
354- ) {
355- #[ cfg( not( target_arch = "wasm32" ) ) ]
356439 let online_cache = Cache :: builder ( )
440+ . dir ( cache_root. clone ( ) )
357441 . progress_bar ( Some ( ProgressBar :: Full ) )
358442 . build ( )
359- . expect ( "Failed to create the online file cache." ) ;
360-
361- #[ cfg( not( target_arch = "wasm32" ) ) ]
443+ . expect ( "failed to create online scene asset cache" ) ;
362444 let offline_cache = Cache :: builder ( )
363- . progress_bar ( Some ( ProgressBar :: Full ) )
445+ . dir ( cache_root )
364446 . offline ( true )
447+ . progress_bar ( None )
365448 . build ( )
366- . expect ( "Failed to create the offline file cache." ) ;
367-
368- #[ cfg( not( target_arch = "wasm32" ) ) ]
369- let balance_bot_url = format ! ( "{BASE_ASSETS_URL}{BALANCEBOT}" ) ;
370-
371- #[ cfg( not( target_arch = "wasm32" ) ) ]
372- let balance_bot_hashed =
373- get_asset_path ( & online_cache, & offline_cache, & balance_bot_url, BALANCEBOT )
374- . expect ( "Failed to get balancebot.glb (online or cached)." ) ;
375-
376- #[ cfg( not( target_arch = "wasm32" ) ) ]
377- let balance_bot_path = balance_bot_hashed. parent ( ) . unwrap ( ) . join ( BALANCEBOT ) ;
378-
379- #[ cfg( not( target_arch = "wasm32" ) ) ]
380- create_symlink (
381- balance_bot_hashed. to_str ( ) . unwrap ( ) ,
382- balance_bot_path. to_str ( ) . unwrap ( ) ,
383- )
384- . expect ( "Failed to create symlink to balancebot.glb." ) ;
385-
386- #[ cfg( not( target_arch = "wasm32" ) ) ]
387- let skybox_url = format ! ( "{BASE_ASSETS_URL}{SKYBOX}" ) ;
388-
389- #[ cfg( not( target_arch = "wasm32" ) ) ]
390- let skybox_path_hashed = get_asset_path ( & online_cache, & offline_cache, & skybox_url, SKYBOX )
391- . expect ( "Failed to get skybox.ktx2 (online or cached)." ) ;
392-
393- #[ cfg( not( target_arch = "wasm32" ) ) ]
394- let skybox_path = skybox_path_hashed. parent ( ) . unwrap ( ) . join ( SKYBOX ) ;
395-
396- #[ cfg( not( target_arch = "wasm32" ) ) ]
397- create_symlink (
398- skybox_path_hashed. to_str ( ) . unwrap ( ) ,
399- skybox_path. to_str ( ) . unwrap ( ) ,
400- )
401- . expect ( "Failed to create symlink to skybox.ktx2." ) ;
402-
403- #[ cfg( not( target_arch = "wasm32" ) ) ]
404- let diffuse_map_url = format ! ( "{BASE_ASSETS_URL}{DIFFUSE_MAP}" ) ;
449+ . expect ( "failed to create offline scene asset cache" ) ;
450+
451+ let total = SCENE_ASSETS . len ( ) ;
452+ let mut paths = Vec :: with_capacity ( total) ;
453+ for ( i, asset_name) in SCENE_ASSETS . iter ( ) . enumerate ( ) {
454+ paths. push (
455+ precached_asset_path (
456+ & online_cache,
457+ & offline_cache,
458+ & asset_root,
459+ i + 1 ,
460+ total,
461+ asset_name,
462+ )
463+ . unwrap_or_else ( |err| panic ! ( "failed to prepare {asset_name}: {err}" ) ) ,
464+ ) ;
465+ }
405466
406- #[ cfg( not( target_arch = "wasm32" ) ) ]
407- let diffuse_map_path_hashed =
408- get_asset_path ( & online_cache, & offline_cache, & diffuse_map_url, DIFFUSE_MAP )
409- . expect ( "Failed to get diffuse_map.ktx2 (online or cached)." ) ;
467+ eprintln ! ( "Scene assets ready." ) ;
468+ SceneAssetPaths {
469+ balance_bot : paths[ 0 ] . clone ( ) ,
470+ skybox : paths[ 1 ] . clone ( ) ,
471+ diffuse_map : paths[ 2 ] . clone ( ) ,
472+ }
473+ }
410474
411- #[ cfg( not( target_arch = "wasm32" ) ) ]
412- let diffuse_map_path = diffuse_map_path_hashed. parent ( ) . unwrap ( ) . join ( DIFFUSE_MAP ) ;
475+ #[ cfg( target_arch = "wasm32" ) ]
476+ fn prepare_scene_assets ( ) -> SceneAssetPaths {
477+ SceneAssetPaths {
478+ balance_bot : BALANCEBOT . to_string ( ) ,
479+ skybox : SKYBOX . to_string ( ) ,
480+ diffuse_map : DIFFUSE_MAP . to_string ( ) ,
481+ }
482+ }
413483
414- #[ cfg( not( target_arch = "wasm32" ) ) ]
415- create_symlink (
416- diffuse_map_path_hashed. to_str ( ) . unwrap ( ) ,
417- diffuse_map_path. to_str ( ) . unwrap ( ) ,
418- )
419- . expect ( "Failed to create symlink to diffuse_map.ktx2." ) ;
420-
421- #[ cfg( target_arch = "wasm32" ) ]
422- let balance_bot_path = BALANCEBOT ;
423- #[ cfg( target_arch = "wasm32" ) ]
424- let skybox_path = SKYBOX ;
425- #[ cfg( target_arch = "wasm32" ) ]
426- let diffuse_map_path = DIFFUSE_MAP ;
427-
428- let scene_handle = asset_server. load ( GltfAssetLabel :: Scene ( 0 ) . from_asset ( balance_bot_path) ) ;
429- let skybox_handle = asset_server. load ( skybox_path) ;
430- let diffuse_map_handle = asset_server. load ( diffuse_map_path) ;
484+ fn setup_scene (
485+ mut commands : Commands ,
486+ asset_server : Res < AssetServer > ,
487+ mut images : ResMut < Assets < Image > > ,
488+ mut meshes : ResMut < Assets < Mesh > > ,
489+ mut materials : ResMut < Assets < StandardMaterial > > ,
490+ layout : Res < WorldLayout > ,
491+ asset_paths : Res < SceneAssetPaths > ,
492+ ) {
493+ let scene_handle =
494+ asset_server. load ( GltfAssetLabel :: Scene ( 0 ) . from_asset ( asset_paths. balance_bot . clone ( ) ) ) ;
495+ let skybox_handle = asset_server. load ( asset_paths. skybox . clone ( ) ) ;
496+ let diffuse_map_handle = asset_server. load ( asset_paths. diffuse_map . clone ( ) ) ;
431497 let specular_map_handle = skybox_handle. clone ( ) ;
432498
433499 commands. insert_resource ( GlobalAmbientLight {
0 commit comments