@@ -58,7 +58,7 @@ use cu_sensor_payloads::{BarometerPayload, ImuPayload, MagnetometerPayload};
5858#[ cfg( not( target_arch = "wasm32" ) ) ]
5959use std:: fs;
6060#[ cfg( not( target_arch = "wasm32" ) ) ]
61- use std:: io;
61+ use std:: io:: { self , Write } ;
6262#[ cfg( not( target_arch = "wasm32" ) ) ]
6363use std:: path:: { Path , PathBuf } ;
6464use std:: sync:: atomic:: Ordering ;
@@ -96,6 +96,14 @@ struct CopperState {
9696 app : gnss:: FlightControllerSim ,
9797}
9898
99+ #[ derive( Clone , Resource ) ]
100+ struct SceneAssetPaths {
101+ quadcopter : String ,
102+ city : String ,
103+ skybox : String ,
104+ specular_map : String ,
105+ }
106+
99107#[ derive( Resource , Default , Clone ) ]
100108struct SimMotorCommands {
101109 dshot : [ u16 ; 4 ] ,
@@ -397,6 +405,9 @@ const SKYBOX: &str = "skybox.ktx2";
397405const SPECULAR_MAP : & str = "specular_map.ktx2" ;
398406const QUADCOPTER : & str = "quadcopter.glb" ;
399407const CITY : & str = "city-fixed.glb" ;
408+ const SCENE_ASSETS : [ & str ; 4 ] = [ QUADCOPTER , CITY , SKYBOX , SPECULAR_MAP ] ;
409+ #[ cfg( not( target_arch = "wasm32" ) ) ]
410+ const SCENE_ASSET_CACHE_DIR : & str = ".download-cache" ;
400411// Measured from `gltf-transform inspect city.glb` in source model units.
401412const LOCAL_CITY_BBOX_MIN_UNITS : Vec3 = Vec3 :: new ( -30_614.165 , -648.2196 , -4_185.883 ) ;
402413const LOCAL_CITY_BBOX_MAX_UNITS : Vec3 = Vec3 :: new ( 18_754.953 , 11_102.407 , 35_871.875 ) ;
@@ -442,21 +453,62 @@ fn spawn_pose_components() -> (
442453}
443454
444455#[ cfg( not( target_arch = "wasm32" ) ) ]
445- fn create_symlink ( src : & str , dst : & str ) -> io:: Result < ( ) > {
446- let dst_path = Path :: new ( dst) ;
456+ fn scene_asset_root ( ) -> PathBuf {
457+ PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( "assets" )
458+ }
459+
460+ #[ cfg( not( target_arch = "wasm32" ) ) ]
461+ fn scene_asset_cache_root ( ) -> PathBuf {
462+ scene_asset_root ( ) . join ( SCENE_ASSET_CACHE_DIR )
463+ }
464+
465+ #[ cfg( not( target_arch = "wasm32" ) ) ]
466+ fn path_string ( path : & Path ) -> String {
467+ path. to_string_lossy ( ) . into_owned ( )
468+ }
469+
470+ #[ cfg( not( target_arch = "wasm32" ) ) ]
471+ fn asset_progress_bar ( done : usize , total : usize ) -> String {
472+ const WIDTH : usize = 28 ;
473+ let filled = WIDTH * done / total. max ( 1 ) ;
474+ let empty = WIDTH . saturating_sub ( filled) ;
475+ format ! (
476+ "[{}{}] {done}/{total}" ,
477+ "=" . repeat( filled) ,
478+ " " . repeat( empty)
479+ )
480+ }
447481
448- if dst_path. exists ( ) {
449- fs:: remove_file ( dst_path) ?;
482+ #[ cfg( not( target_arch = "wasm32" ) ) ]
483+ fn link_or_copy_cached_asset ( src : & Path , dst : & Path ) -> io:: Result < ( ) > {
484+ if fs:: symlink_metadata ( dst) . is_ok ( ) {
485+ fs:: remove_file ( dst) ?;
450486 }
451487
452488 #[ cfg( unix) ]
453489 {
454- std:: os:: unix:: fs:: symlink ( src, dst)
490+ match std:: os:: unix:: fs:: symlink ( src, dst) {
491+ Ok ( ( ) ) => Ok ( ( ) ) ,
492+ Err ( symlink_err) => fs:: copy ( src, dst) . map ( |_| ( ) ) . map_err ( |copy_err| {
493+ io:: Error :: new (
494+ copy_err. kind ( ) ,
495+ format ! ( "failed to symlink ({symlink_err}) or copy ({copy_err})" ) ,
496+ )
497+ } ) ,
498+ }
455499 }
456500
457501 #[ cfg( windows) ]
458502 {
459- std:: os:: windows:: fs:: symlink_file ( src, dst)
503+ match std:: os:: windows:: fs:: symlink_file ( src, dst) {
504+ Ok ( ( ) ) => Ok ( ( ) ) ,
505+ Err ( symlink_err) => fs:: copy ( src, dst) . map ( |_| ( ) ) . map_err ( |copy_err| {
506+ io:: Error :: new (
507+ copy_err. kind ( ) ,
508+ format ! ( "failed to symlink ({symlink_err}) or copy ({copy_err})" ) ,
509+ )
510+ } ) ,
511+ }
460512 }
461513}
462514
@@ -467,18 +519,15 @@ fn get_asset_path(
467519 asset_url : & str ,
468520 asset_name : & str ,
469521) -> Result < PathBuf , CacheError > {
470- match online_cache . cached_path ( asset_url) {
522+ match offline_cache . cached_path ( asset_url) {
471523 Ok ( path) => Ok ( path) ,
472524 Err ( err) => {
473525 if matches ! (
474526 err,
475- CacheError :: HttpError ( _) | CacheError :: IoError ( _ ) | CacheError :: ResourceNotFound ( _)
527+ CacheError :: NoCachedVersions ( _) | CacheError :: CacheCorrupted ( _)
476528 ) {
477- eprintln ! (
478- "Failed to fetch latest '{}' from network ({}). Attempting to use cached version." ,
479- asset_name, err
480- ) ;
481- offline_cache. cached_path ( asset_url)
529+ eprintln ! ( " {asset_name}: cache miss; downloading from {asset_url}" ) ;
530+ online_cache. cached_path ( asset_url)
482531 } else {
483532 Err ( err)
484533 }
@@ -490,14 +539,92 @@ fn get_asset_path(
490539fn precached_asset_path (
491540 online_cache : & Cache ,
492541 offline_cache : & Cache ,
542+ asset_root : & Path ,
543+ index : usize ,
544+ total : usize ,
493545 asset_name : & str ,
494- ) -> Result < PathBuf , CacheError > {
546+ ) -> Result < String , CacheError > {
547+ let plain_path = asset_root. join ( asset_name) ;
548+ if plain_path. is_file ( ) {
549+ eprintln ! (
550+ " {} {asset_name}: cached" ,
551+ asset_progress_bar( index, total)
552+ ) ;
553+ return Ok ( path_string ( & plain_path) ) ;
554+ }
555+
556+ if fs:: symlink_metadata ( & plain_path) . is_ok ( ) {
557+ fs:: remove_file ( & plain_path) ?;
558+ }
559+
560+ eprintln ! (
561+ " {} {asset_name}: resolving" ,
562+ asset_progress_bar( index. saturating_sub( 1 ) , total)
563+ ) ;
495564 let asset_url = format ! ( "{BASE_ASSETS_URL}{asset_name}" ) ;
496565 let hashed_path = get_asset_path ( online_cache, offline_cache, & asset_url, asset_name) ?;
497- let plain_path = hashed_path. parent ( ) . unwrap ( ) . join ( asset_name) ;
498- create_symlink ( hashed_path. to_str ( ) . unwrap ( ) , plain_path. to_str ( ) . unwrap ( ) )
499- . expect ( "failed to create cached-asset symlink" ) ;
500- Ok ( plain_path)
566+ link_or_copy_cached_asset ( & hashed_path, & plain_path) ?;
567+ eprintln ! ( " {} {asset_name}: ready" , asset_progress_bar( index, total) ) ;
568+ Ok ( path_string ( & plain_path) )
569+ }
570+
571+ #[ cfg( not( target_arch = "wasm32" ) ) ]
572+ fn prepare_scene_assets ( ) -> SceneAssetPaths {
573+ let asset_root = scene_asset_root ( ) ;
574+ let cache_root = scene_asset_cache_root ( ) ;
575+ fs:: create_dir_all ( & asset_root) . expect ( "failed to create scene asset directory" ) ;
576+
577+ eprintln ! (
578+ "Preparing Copper flight-controller scene assets in {}" ,
579+ asset_root. display( )
580+ ) ;
581+ let _ = io:: stderr ( ) . flush ( ) ;
582+
583+ let online_cache = Cache :: builder ( )
584+ . dir ( cache_root. clone ( ) )
585+ . progress_bar ( Some ( ProgressBar :: Full ) )
586+ . build ( )
587+ . expect ( "failed to create online scene asset cache" ) ;
588+ let offline_cache = Cache :: builder ( )
589+ . dir ( cache_root)
590+ . offline ( true )
591+ . progress_bar ( None )
592+ . build ( )
593+ . expect ( "failed to create offline scene asset cache" ) ;
594+
595+ let total = SCENE_ASSETS . len ( ) ;
596+ let mut paths = Vec :: with_capacity ( total) ;
597+ for ( i, asset_name) in SCENE_ASSETS . iter ( ) . enumerate ( ) {
598+ paths. push (
599+ precached_asset_path (
600+ & online_cache,
601+ & offline_cache,
602+ & asset_root,
603+ i + 1 ,
604+ total,
605+ asset_name,
606+ )
607+ . unwrap_or_else ( |err| panic ! ( "failed to prepare {asset_name}: {err}" ) ) ,
608+ ) ;
609+ }
610+
611+ eprintln ! ( "Scene assets ready." ) ;
612+ SceneAssetPaths {
613+ quadcopter : paths[ 0 ] . clone ( ) ,
614+ city : paths[ 1 ] . clone ( ) ,
615+ skybox : paths[ 2 ] . clone ( ) ,
616+ specular_map : paths[ 3 ] . clone ( ) ,
617+ }
618+ }
619+
620+ #[ cfg( target_arch = "wasm32" ) ]
621+ fn prepare_scene_assets ( ) -> SceneAssetPaths {
622+ SceneAssetPaths {
623+ quadcopter : QUADCOPTER . to_string ( ) ,
624+ city : CITY . to_string ( ) ,
625+ skybox : SKYBOX . to_string ( ) ,
626+ specular_map : SPECULAR_MAP . to_string ( ) ,
627+ }
501628}
502629
503630fn propeller_from_position ( x : f32 , y : f32 , z : f32 , direction : RotationDirection ) -> PropellerInfo {
@@ -696,52 +823,15 @@ fn setup_world(
696823 asset_server : Res < AssetServer > ,
697824 mut images : ResMut < Assets < Image > > ,
698825 layout : Res < WorldLayout > ,
826+ asset_paths : Res < SceneAssetPaths > ,
699827) {
700- #[ cfg( not( target_arch = "wasm32" ) ) ]
701- let online_cache = Cache :: builder ( )
702- . progress_bar ( Some ( ProgressBar :: Full ) )
703- . build ( )
704- . expect ( "failed to create online file cache" ) ;
705-
706- #[ cfg( not( target_arch = "wasm32" ) ) ]
707- let offline_cache = Cache :: builder ( )
708- . progress_bar ( Some ( ProgressBar :: Full ) )
709- . offline ( true )
710- . build ( )
711- . expect ( "failed to create offline file cache" ) ;
712-
713- #[ cfg( not( target_arch = "wasm32" ) ) ]
714- let quadcopter_path = precached_asset_path ( & online_cache, & offline_cache, QUADCOPTER )
715- . expect ( "failed to get quadcopter.glb (online or cached)" ) ;
716- #[ cfg( not( target_arch = "wasm32" ) ) ]
717- let skybox_path = precached_asset_path ( & online_cache, & offline_cache, SKYBOX )
718- . expect ( "failed to get skybox.ktx2 (online or cached)" ) ;
719- #[ cfg( not( target_arch = "wasm32" ) ) ]
720- let specular_map_path = precached_asset_path ( & online_cache, & offline_cache, SPECULAR_MAP )
721- . expect ( "failed to get specular_map.ktx2 (online or cached)" ) ;
722- #[ cfg( not( target_arch = "wasm32" ) ) ]
723- let city_path = precached_asset_path ( & online_cache, & offline_cache, CITY )
724- . expect ( "failed to get city.glb (online or cached)" ) ;
725- #[ cfg( target_arch = "wasm32" ) ]
726- let quadcopter_path = QUADCOPTER ;
727- #[ cfg( target_arch = "wasm32" ) ]
728- let skybox_path = SKYBOX ;
729- #[ cfg( target_arch = "wasm32" ) ]
730- let specular_map_path = SPECULAR_MAP ;
731- #[ cfg( target_arch = "wasm32" ) ]
732- let city_path = CITY ;
733-
734828 let city_size_units = LOCAL_CITY_BBOX_MAX_UNITS - LOCAL_CITY_BBOX_MIN_UNITS ;
735829 let city_size_m = city_size_units * LOCAL_CITY_SCALE ;
736830 let city_translation = Vec3 :: ZERO ;
737831 let city_scale = Vec3 :: splat ( LOCAL_CITY_SCALE ) ;
738- #[ cfg( not( target_arch = "wasm32" ) ) ]
739- let city_path_str = city_path. to_string_lossy ( ) . into_owned ( ) ;
740- #[ cfg( target_arch = "wasm32" ) ]
741- let city_path_str = city_path. to_string ( ) ;
742832 info ! (
743833 "sim world: loading city {} (bbox {}x{}x{} units, scaled to {}x{}x{} m) with translation ({}, {}, {})" ,
744- city_path_str ,
834+ asset_paths . city . as_str ( ) ,
745835 city_size_units. x,
746836 city_size_units. y,
747837 city_size_units. z,
@@ -753,8 +843,8 @@ fn setup_world(
753843 city_translation. z
754844 ) ;
755845
756- let skybox_handle = asset_server. load ( skybox_path ) ;
757- let specular_map_handle = asset_server. load ( specular_map_path ) ;
846+ let skybox_handle = asset_server. load ( asset_paths . skybox . clone ( ) ) ;
847+ let specular_map_handle = asset_server. load ( asset_paths . specular_map . clone ( ) ) ;
758848
759849 commands. insert_resource ( GlobalAmbientLight {
760850 color : Color :: WHITE ,
@@ -821,14 +911,8 @@ fn setup_world(
821911 Transform :: from_translation ( Vec3 :: new ( 3.0 , 10.0 , 1.0 ) ) . looking_at ( Vec3 :: ZERO , Vec3 :: Y ) ,
822912 ) ) ;
823913
824- #[ cfg( not( target_arch = "wasm32" ) ) ]
825- let quadcopter_scene_path = format ! ( "{}#scene0" , quadcopter_path. display( ) ) ;
826- #[ cfg( target_arch = "wasm32" ) ]
827- let quadcopter_scene_path = format ! ( "{quadcopter_path}#scene0" ) ;
828- #[ cfg( not( target_arch = "wasm32" ) ) ]
829- let city_scene_path = format ! ( "{}#scene0" , city_path. display( ) ) ;
830- #[ cfg( target_arch = "wasm32" ) ]
831- let city_scene_path = format ! ( "{city_path}#scene0" ) ;
914+ let quadcopter_scene_path = format ! ( "{}#scene0" , asset_paths. quadcopter. as_str( ) ) ;
915+ let city_scene_path = format ! ( "{}#scene0" , asset_paths. city. as_str( ) ) ;
832916
833917 let quadcopter_scene =
834918 asset_server. load ( GltfAssetLabel :: Scene ( 0 ) . from_asset ( quadcopter_scene_path) ) ;
@@ -2104,6 +2188,14 @@ fn sync_loading_overlay(
21042188}
21052189
21062190pub fn build_world ( headless : bool , split_monitor : bool ) -> App {
2191+ build_world_with_assets ( headless, split_monitor, None )
2192+ }
2193+
2194+ fn build_world_with_assets (
2195+ headless : bool ,
2196+ split_monitor : bool ,
2197+ scene_assets : Option < SceneAssetPaths > ,
2198+ ) -> App {
21072199 let mut app = App :: new ( ) ;
21082200 app. insert_resource ( SimState :: default ( ) )
21092201 . insert_resource ( SimMotorCommands :: default ( ) )
@@ -2118,6 +2210,10 @@ pub fn build_world(headless: bool, split_monitor: bool) -> App {
21182210 . init_resource :: < SceneLoadState > ( )
21192211 . init_resource :: < SimHudSpawnState > ( ) ;
21202212
2213+ if !headless {
2214+ app. insert_resource ( scene_assets. unwrap_or_else ( prepare_scene_assets) ) ;
2215+ }
2216+
21212217 if headless {
21222218 app. add_plugins ( MinimalPlugins ) ;
21232219 return app;
@@ -2181,7 +2277,8 @@ pub fn build_world(headless: bool, split_monitor: bool) -> App {
21812277
21822278#[ cfg( feature = "sim" ) ]
21832279pub fn run_sim ( ) {
2184- let mut app = build_world ( false , false ) ;
2280+ let scene_assets = prepare_scene_assets ( ) ;
2281+ let mut app = build_world_with_assets ( false , false , Some ( scene_assets) ) ;
21852282 #[ cfg( not( target_arch = "wasm32" ) ) ]
21862283 {
21872284 app. add_systems ( Startup , setup_copper) ;
@@ -2198,9 +2295,10 @@ pub fn run_sim() {
21982295
21992296#[ cfg( feature = "bevymon" ) ]
22002297pub fn run_bevymon ( ) {
2298+ let scene_assets = prepare_scene_assets ( ) ;
22012299 let ( monitor_model, copper) = build_bevymon_copper ( ) ;
22022300
2203- let mut app = build_world ( false , true ) ;
2301+ let mut app = build_world_with_assets ( false , true , Some ( scene_assets ) ) ;
22042302 app. insert_resource ( copper)
22052303 . init_resource :: < LayoutSpawned > ( )
22062304 . add_plugins (
0 commit comments