host.camera_visualizer.field_cache
Field asset cache: downloads AdvantageScope field bundles from GitHub and extracts individual field models (GLB + config.json).
Cached locally in cache/fields/
1""" 2Field asset cache: downloads AdvantageScope field bundles from GitHub 3and extracts individual field models (GLB + config.json). 4 5Cached locally in cache/fields/<field-id>/. 6""" 7 8import io 9import json 10import os 11import urllib.request 12import zipfile 13 14BUNDLE_URL = ( 15 'https://github.com/Mechanical-Advantage/AdvantageScopeAssets' 16 '/releases/download/bundles-v1/AllAssetsDefaultFRC.zip' 17) 18 19# Curated field definitions — id maps to zip entry in the bundle 20FIELD_LIST = [ 21 { 22 'id': 'robot-view', 23 'name': 'Robot View', 24 'description': 'Robot at origin with AprilTag ring', 25 'type': 'builtin', 26 }, 27 { 28 'id': 'evergreen', 29 'name': 'Evergreen Field', 30 'description': 'Standard FRC field perimeter (no game elements)', 31 'type': 'builtin', 32 }, 33 { 34 'id': '2026-field', 35 'name': '2026 Field', 36 'description': '2026 FRC Rebuilt field with game elements', 37 'zipEntry': 'Field3d_2026FRCFieldV1.zip', 38 'type': 'field', 39 }, 40 { 41 'id': '2025-welded', 42 'name': '2025 Field (Welded)', 43 'description': '2025 FRC field, welded variant', 44 'zipEntry': 'Field3d_2025FRCFieldWeldedV2.zip', 45 'type': 'field', 46 }, 47 { 48 'id': '2024-field', 49 'name': '2024 Field', 50 'description': '2024 FRC field', 51 'zipEntry': 'Field3d_2024FRCFieldV3.zip', 52 'type': 'field', 53 }, 54 { 55 'id': 'cat-box', 56 'name': 'Cat Box', 57 'description': 'Meow', 58 'type': 'builtin', 59 }, 60] 61 62 63class FieldCache: 64 """Manages downloading and caching of FRC field models.""" 65 66 def __init__(self, cache_dir: str, bundle_cache_path: str = None): 67 self.cache_dir = cache_dir 68 self.bundle_path = bundle_cache_path or os.path.join( 69 os.path.dirname(cache_dir), 'frc_assets.zip' 70 ) 71 72 def get_field_list(self) -> list: 73 return FIELD_LIST 74 75 def get_field_info(self, field_id: str) -> dict: 76 """Look up a field by id. Returns None if not found.""" 77 return next((f for f in FIELD_LIST if f['id'] == field_id), None) 78 79 def ensure_bundle(self) -> bool: 80 """Download the asset bundle if not cached. Returns True on success.""" 81 if os.path.isfile(self.bundle_path): 82 return True 83 os.makedirs(os.path.dirname(self.bundle_path), exist_ok=True) 84 try: 85 print(f'Downloading field assets from GitHub...') 86 urllib.request.urlretrieve(BUNDLE_URL, self.bundle_path) 87 print(f'Downloaded to {self.bundle_path}') 88 return True 89 except Exception as e: 90 print(f'Failed to download assets: {e}') 91 return False 92 93 def extract_field(self, field_id: str): 94 """Extract a field's model.glb and config.json. 95 96 Returns (config_dict, glb_bytes) or (None, None) on failure. 97 """ 98 info = self.get_field_info(field_id) 99 if not info or 'zipEntry' not in info: 100 return None, None 101 102 # Check local cache 103 field_dir = os.path.join(self.cache_dir, field_id) 104 glb_path = os.path.join(field_dir, 'model.glb') 105 config_path = os.path.join(field_dir, 'config.json') 106 107 if os.path.isfile(glb_path) and os.path.isfile(config_path): 108 with open(config_path, 'r') as f: 109 config = json.load(f) 110 with open(glb_path, 'rb') as f: 111 glb = f.read() 112 return config, glb 113 114 # Extract from bundle 115 if not self.ensure_bundle(): 116 return None, None 117 118 try: 119 outer = zipfile.ZipFile(self.bundle_path) 120 inner_data = outer.read(info['zipEntry']) 121 inner = zipfile.ZipFile(io.BytesIO(inner_data)) 122 123 config = json.loads(inner.read('config.json')) 124 glb = inner.read('model.glb') 125 126 # Cache locally 127 os.makedirs(field_dir, exist_ok=True) 128 with open(config_path, 'w') as f: 129 json.dump(config, f) 130 with open(glb_path, 'wb') as f: 131 f.write(glb) 132 133 return config, glb 134 except Exception as e: 135 print(f'Failed to extract field {field_id}: {e}') 136 return None, None
BUNDLE_URL =
'https://github.com/Mechanical-Advantage/AdvantageScopeAssets/releases/download/bundles-v1/AllAssetsDefaultFRC.zip'
FIELD_LIST =
[{'id': 'robot-view', 'name': 'Robot View', 'description': 'Robot at origin with AprilTag ring', 'type': 'builtin'}, {'id': 'evergreen', 'name': 'Evergreen Field', 'description': 'Standard FRC field perimeter (no game elements)', 'type': 'builtin'}, {'id': '2026-field', 'name': '2026 Field', 'description': '2026 FRC Rebuilt field with game elements', 'zipEntry': 'Field3d_2026FRCFieldV1.zip', 'type': 'field'}, {'id': '2025-welded', 'name': '2025 Field (Welded)', 'description': '2025 FRC field, welded variant', 'zipEntry': 'Field3d_2025FRCFieldWeldedV2.zip', 'type': 'field'}, {'id': '2024-field', 'name': '2024 Field', 'description': '2024 FRC field', 'zipEntry': 'Field3d_2024FRCFieldV3.zip', 'type': 'field'}, {'id': 'cat-box', 'name': 'Cat Box', 'description': 'Meow', 'type': 'builtin'}]
class
FieldCache:
64class FieldCache: 65 """Manages downloading and caching of FRC field models.""" 66 67 def __init__(self, cache_dir: str, bundle_cache_path: str = None): 68 self.cache_dir = cache_dir 69 self.bundle_path = bundle_cache_path or os.path.join( 70 os.path.dirname(cache_dir), 'frc_assets.zip' 71 ) 72 73 def get_field_list(self) -> list: 74 return FIELD_LIST 75 76 def get_field_info(self, field_id: str) -> dict: 77 """Look up a field by id. Returns None if not found.""" 78 return next((f for f in FIELD_LIST if f['id'] == field_id), None) 79 80 def ensure_bundle(self) -> bool: 81 """Download the asset bundle if not cached. Returns True on success.""" 82 if os.path.isfile(self.bundle_path): 83 return True 84 os.makedirs(os.path.dirname(self.bundle_path), exist_ok=True) 85 try: 86 print(f'Downloading field assets from GitHub...') 87 urllib.request.urlretrieve(BUNDLE_URL, self.bundle_path) 88 print(f'Downloaded to {self.bundle_path}') 89 return True 90 except Exception as e: 91 print(f'Failed to download assets: {e}') 92 return False 93 94 def extract_field(self, field_id: str): 95 """Extract a field's model.glb and config.json. 96 97 Returns (config_dict, glb_bytes) or (None, None) on failure. 98 """ 99 info = self.get_field_info(field_id) 100 if not info or 'zipEntry' not in info: 101 return None, None 102 103 # Check local cache 104 field_dir = os.path.join(self.cache_dir, field_id) 105 glb_path = os.path.join(field_dir, 'model.glb') 106 config_path = os.path.join(field_dir, 'config.json') 107 108 if os.path.isfile(glb_path) and os.path.isfile(config_path): 109 with open(config_path, 'r') as f: 110 config = json.load(f) 111 with open(glb_path, 'rb') as f: 112 glb = f.read() 113 return config, glb 114 115 # Extract from bundle 116 if not self.ensure_bundle(): 117 return None, None 118 119 try: 120 outer = zipfile.ZipFile(self.bundle_path) 121 inner_data = outer.read(info['zipEntry']) 122 inner = zipfile.ZipFile(io.BytesIO(inner_data)) 123 124 config = json.loads(inner.read('config.json')) 125 glb = inner.read('model.glb') 126 127 # Cache locally 128 os.makedirs(field_dir, exist_ok=True) 129 with open(config_path, 'w') as f: 130 json.dump(config, f) 131 with open(glb_path, 'wb') as f: 132 f.write(glb) 133 134 return config, glb 135 except Exception as e: 136 print(f'Failed to extract field {field_id}: {e}') 137 return None, None
Manages downloading and caching of FRC field models.
def
get_field_info(self, field_id: str) -> dict:
76 def get_field_info(self, field_id: str) -> dict: 77 """Look up a field by id. Returns None if not found.""" 78 return next((f for f in FIELD_LIST if f['id'] == field_id), None)
Look up a field by id. Returns None if not found.
def
ensure_bundle(self) -> bool:
80 def ensure_bundle(self) -> bool: 81 """Download the asset bundle if not cached. Returns True on success.""" 82 if os.path.isfile(self.bundle_path): 83 return True 84 os.makedirs(os.path.dirname(self.bundle_path), exist_ok=True) 85 try: 86 print(f'Downloading field assets from GitHub...') 87 urllib.request.urlretrieve(BUNDLE_URL, self.bundle_path) 88 print(f'Downloaded to {self.bundle_path}') 89 return True 90 except Exception as e: 91 print(f'Failed to download assets: {e}') 92 return False
Download the asset bundle if not cached. Returns True on success.
def
extract_field(self, field_id: str):
94 def extract_field(self, field_id: str): 95 """Extract a field's model.glb and config.json. 96 97 Returns (config_dict, glb_bytes) or (None, None) on failure. 98 """ 99 info = self.get_field_info(field_id) 100 if not info or 'zipEntry' not in info: 101 return None, None 102 103 # Check local cache 104 field_dir = os.path.join(self.cache_dir, field_id) 105 glb_path = os.path.join(field_dir, 'model.glb') 106 config_path = os.path.join(field_dir, 'config.json') 107 108 if os.path.isfile(glb_path) and os.path.isfile(config_path): 109 with open(config_path, 'r') as f: 110 config = json.load(f) 111 with open(glb_path, 'rb') as f: 112 glb = f.read() 113 return config, glb 114 115 # Extract from bundle 116 if not self.ensure_bundle(): 117 return None, None 118 119 try: 120 outer = zipfile.ZipFile(self.bundle_path) 121 inner_data = outer.read(info['zipEntry']) 122 inner = zipfile.ZipFile(io.BytesIO(inner_data)) 123 124 config = json.loads(inner.read('config.json')) 125 glb = inner.read('model.glb') 126 127 # Cache locally 128 os.makedirs(field_dir, exist_ok=True) 129 with open(config_path, 'w') as f: 130 json.dump(config, f) 131 with open(glb_path, 'wb') as f: 132 f.write(glb) 133 134 return config, glb 135 except Exception as e: 136 print(f'Failed to extract field {field_id}: {e}') 137 return None, None
Extract a field's model.glb and config.json.
Returns (config_dict, glb_bytes) or (None, None) on failure.