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.

FieldCache(cache_dir: str, bundle_cache_path: str = None)
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        )
cache_dir
bundle_path
def get_field_list(self) -> list:
73    def get_field_list(self) -> list:
74        return FIELD_LIST
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.