kicad-sch-api 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +112 -0
- kicad_sch_api/core/__init__.py +23 -0
- kicad_sch_api/core/components.py +652 -0
- kicad_sch_api/core/formatter.py +312 -0
- kicad_sch_api/core/parser.py +434 -0
- kicad_sch_api/core/schematic.py +478 -0
- kicad_sch_api/core/types.py +369 -0
- kicad_sch_api/library/__init__.py +10 -0
- kicad_sch_api/library/cache.py +548 -0
- kicad_sch_api/mcp/__init__.py +5 -0
- kicad_sch_api/mcp/server.py +500 -0
- kicad_sch_api/py.typed +1 -0
- kicad_sch_api/utils/__init__.py +15 -0
- kicad_sch_api/utils/validation.py +447 -0
- kicad_sch_api-0.0.1.dist-info/METADATA +226 -0
- kicad_sch_api-0.0.1.dist-info/RECORD +20 -0
- kicad_sch_api-0.0.1.dist-info/WHEEL +5 -0
- kicad_sch_api-0.0.1.dist-info/entry_points.txt +2 -0
- kicad_sch_api-0.0.1.dist-info/licenses/LICENSE +21 -0
- kicad_sch_api-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance symbol library cache for KiCAD schematic API.
|
|
3
|
+
|
|
4
|
+
This module provides intelligent caching and lookup functionality for KiCAD symbol libraries,
|
|
5
|
+
significantly improving performance for applications that work with many components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from ..core.types import PinShape, PinType, Point, SchematicPin
|
|
18
|
+
from ..utils.validation import ValidationError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SymbolDefinition:
|
|
25
|
+
"""Complete definition of a symbol from KiCAD library."""
|
|
26
|
+
|
|
27
|
+
lib_id: str # e.g., "Device:R"
|
|
28
|
+
name: str # Symbol name within library
|
|
29
|
+
library: str # Library name
|
|
30
|
+
reference_prefix: str # e.g., "R" for resistors
|
|
31
|
+
description: str = ""
|
|
32
|
+
keywords: str = ""
|
|
33
|
+
datasheet: str = ""
|
|
34
|
+
pins: List[SchematicPin] = field(default_factory=list)
|
|
35
|
+
units: int = 1
|
|
36
|
+
unit_names: Dict[int, str] = field(default_factory=dict)
|
|
37
|
+
power_symbol: bool = False
|
|
38
|
+
graphic_elements: List[Dict[str, Any]] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
# Performance metrics
|
|
41
|
+
load_time: float = 0.0
|
|
42
|
+
access_count: int = 0
|
|
43
|
+
last_accessed: float = field(default_factory=time.time)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
"""Post-initialization processing."""
|
|
47
|
+
self.last_accessed = time.time()
|
|
48
|
+
|
|
49
|
+
# Validate lib_id format
|
|
50
|
+
if ":" not in self.lib_id:
|
|
51
|
+
raise ValidationError(
|
|
52
|
+
f"Invalid lib_id format: {self.lib_id} (should be Library:Symbol)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Extract library from lib_id if not provided
|
|
56
|
+
if not self.library:
|
|
57
|
+
self.library = self.lib_id.split(":")[0]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def bounding_box(self) -> Tuple[float, float, float, float]:
|
|
61
|
+
"""
|
|
62
|
+
Calculate symbol bounding box from graphic elements and pins.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
(min_x, min_y, max_x, max_y) in mm
|
|
66
|
+
"""
|
|
67
|
+
if not self.graphic_elements and not self.pins:
|
|
68
|
+
# Default bounding box for empty symbol
|
|
69
|
+
return (-2.54, -2.54, 2.54, 2.54)
|
|
70
|
+
|
|
71
|
+
coordinates = []
|
|
72
|
+
|
|
73
|
+
# Collect pin positions
|
|
74
|
+
for pin in self.pins:
|
|
75
|
+
coordinates.extend([(pin.position.x, pin.position.y)])
|
|
76
|
+
|
|
77
|
+
# Collect graphic element coordinates
|
|
78
|
+
for elem in self.graphic_elements:
|
|
79
|
+
if "points" in elem:
|
|
80
|
+
coordinates.extend(elem["points"])
|
|
81
|
+
elif "center" in elem and "radius" in elem:
|
|
82
|
+
# Circle - approximate with bounding box
|
|
83
|
+
cx, cy = elem["center"]
|
|
84
|
+
radius = elem["radius"]
|
|
85
|
+
coordinates.extend([(cx - radius, cy - radius), (cx + radius, cy + radius)])
|
|
86
|
+
|
|
87
|
+
if not coordinates:
|
|
88
|
+
return (-2.54, -2.54, 2.54, 2.54)
|
|
89
|
+
|
|
90
|
+
min_x = min(coord[0] for coord in coordinates)
|
|
91
|
+
max_x = max(coord[0] for coord in coordinates)
|
|
92
|
+
min_y = min(coord[1] for coord in coordinates)
|
|
93
|
+
max_y = max(coord[1] for coord in coordinates)
|
|
94
|
+
|
|
95
|
+
return (min_x, min_y, max_x, max_y)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def size(self) -> Tuple[float, float]:
|
|
99
|
+
"""Get symbol size (width, height) in mm."""
|
|
100
|
+
min_x, min_y, max_x, max_y = self.bounding_box
|
|
101
|
+
return (max_x - min_x, max_y - min_y)
|
|
102
|
+
|
|
103
|
+
def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
|
|
104
|
+
"""Get pin by number."""
|
|
105
|
+
for pin in self.pins:
|
|
106
|
+
if pin.number == pin_number:
|
|
107
|
+
pin.name # Access pin to update symbol statistics
|
|
108
|
+
self.access_count += 1
|
|
109
|
+
self.last_accessed = time.time()
|
|
110
|
+
return pin
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def get_pins_by_type(self, pin_type: PinType) -> List[SchematicPin]:
|
|
114
|
+
"""Get all pins of specified type."""
|
|
115
|
+
self.access_count += 1
|
|
116
|
+
self.last_accessed = time.time()
|
|
117
|
+
return [pin for pin in self.pins if pin.pin_type == pin_type]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class LibraryStats:
|
|
122
|
+
"""Statistics for symbol library performance tracking."""
|
|
123
|
+
|
|
124
|
+
library_path: Path
|
|
125
|
+
symbol_count: int = 0
|
|
126
|
+
load_time: float = 0.0
|
|
127
|
+
file_size: int = 0
|
|
128
|
+
last_modified: float = 0.0
|
|
129
|
+
cache_hit_rate: float = 0.0
|
|
130
|
+
access_count: int = 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SymbolLibraryCache:
|
|
134
|
+
"""
|
|
135
|
+
High-performance cache for KiCAD symbol libraries.
|
|
136
|
+
|
|
137
|
+
Features:
|
|
138
|
+
- Intelligent caching with performance metrics
|
|
139
|
+
- Fast symbol lookup and indexing
|
|
140
|
+
- Library discovery and management
|
|
141
|
+
- Memory-efficient storage
|
|
142
|
+
- Cache invalidation based on file modification time
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, cache_dir: Optional[Path] = None, enable_persistence: bool = True):
|
|
146
|
+
"""
|
|
147
|
+
Initialize the symbol cache.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
cache_dir: Directory to store cached symbol data
|
|
151
|
+
enable_persistence: Whether to persist cache to disk
|
|
152
|
+
"""
|
|
153
|
+
self._symbols: Dict[str, SymbolDefinition] = {}
|
|
154
|
+
self._library_paths: Set[Path] = set()
|
|
155
|
+
|
|
156
|
+
# Cache configuration
|
|
157
|
+
self._cache_dir = cache_dir or Path.home() / ".cache" / "kicad-sch-api" / "symbols"
|
|
158
|
+
self._enable_persistence = enable_persistence
|
|
159
|
+
|
|
160
|
+
if enable_persistence:
|
|
161
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
# Indexes for fast lookup
|
|
164
|
+
self._symbol_index: Dict[str, str] = {} # symbol_name -> lib_id
|
|
165
|
+
self._library_index: Dict[str, Path] = {} # library_name -> path
|
|
166
|
+
self._lib_stats: Dict[str, LibraryStats] = {}
|
|
167
|
+
|
|
168
|
+
# Performance tracking
|
|
169
|
+
self._cache_hits = 0
|
|
170
|
+
self._cache_misses = 0
|
|
171
|
+
self._total_load_time = 0.0
|
|
172
|
+
|
|
173
|
+
# Load persistent cache if available
|
|
174
|
+
self._index_file = self._cache_dir / "symbol_index.json" if enable_persistence else None
|
|
175
|
+
if enable_persistence:
|
|
176
|
+
self._load_persistent_index()
|
|
177
|
+
|
|
178
|
+
logger.info(f"Symbol cache initialized (persistence: {enable_persistence})")
|
|
179
|
+
|
|
180
|
+
def add_library_path(self, library_path: Union[str, Path]) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Add a library path to the cache.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
library_path: Path to .kicad_sym file
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if library was added successfully
|
|
189
|
+
"""
|
|
190
|
+
library_path = Path(library_path)
|
|
191
|
+
|
|
192
|
+
if not library_path.exists():
|
|
193
|
+
logger.warning(f"Library file not found: {library_path}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
if not library_path.suffix == ".kicad_sym":
|
|
197
|
+
logger.warning(f"Not a KiCAD symbol library: {library_path}")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if library_path in self._library_paths:
|
|
201
|
+
logger.debug(f"Library already in cache: {library_path}")
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
self._library_paths.add(library_path)
|
|
205
|
+
library_name = library_path.stem
|
|
206
|
+
self._library_index[library_name] = library_path
|
|
207
|
+
|
|
208
|
+
# Initialize library statistics
|
|
209
|
+
stat = library_path.stat()
|
|
210
|
+
self._lib_stats[library_name] = LibraryStats(
|
|
211
|
+
library_path=library_path, file_size=stat.st_size, last_modified=stat.st_mtime
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
logger.info(f"Added library: {library_name} ({library_path})")
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def discover_libraries(self, search_paths: List[Union[str, Path]] = None) -> int:
|
|
218
|
+
"""
|
|
219
|
+
Automatically discover KiCAD symbol libraries.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
search_paths: Directories to search for .kicad_sym files
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Number of libraries discovered and added
|
|
226
|
+
"""
|
|
227
|
+
if search_paths is None:
|
|
228
|
+
search_paths = self._get_default_library_paths()
|
|
229
|
+
|
|
230
|
+
discovered_count = 0
|
|
231
|
+
|
|
232
|
+
for search_path in search_paths:
|
|
233
|
+
search_path = Path(search_path)
|
|
234
|
+
if not search_path.exists():
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
logger.info(f"Discovering libraries in: {search_path}")
|
|
238
|
+
|
|
239
|
+
# Find all .kicad_sym files
|
|
240
|
+
for lib_file in search_path.rglob("*.kicad_sym"):
|
|
241
|
+
if self.add_library_path(lib_file):
|
|
242
|
+
discovered_count += 1
|
|
243
|
+
|
|
244
|
+
logger.info(f"Discovered {discovered_count} libraries")
|
|
245
|
+
return discovered_count
|
|
246
|
+
|
|
247
|
+
def get_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
|
|
248
|
+
"""
|
|
249
|
+
Get symbol definition by lib_id.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
lib_id: Symbol identifier (e.g., "Device:R")
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Symbol definition if found, None otherwise
|
|
256
|
+
"""
|
|
257
|
+
# Check cache first
|
|
258
|
+
if lib_id in self._symbols:
|
|
259
|
+
self._cache_hits += 1
|
|
260
|
+
symbol = self._symbols[lib_id]
|
|
261
|
+
symbol.access_count += 1
|
|
262
|
+
symbol.last_accessed = time.time()
|
|
263
|
+
return symbol
|
|
264
|
+
|
|
265
|
+
# Cache miss - try to load symbol
|
|
266
|
+
self._cache_misses += 1
|
|
267
|
+
return self._load_symbol(lib_id)
|
|
268
|
+
|
|
269
|
+
def search_symbols(
|
|
270
|
+
self, query: str, library: Optional[str] = None, limit: int = 50
|
|
271
|
+
) -> List[SymbolDefinition]:
|
|
272
|
+
"""
|
|
273
|
+
Search for symbols by name, description, or keywords.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
query: Search query string
|
|
277
|
+
library: Optional library name to search within
|
|
278
|
+
limit: Maximum number of results
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of matching symbol definitions
|
|
282
|
+
"""
|
|
283
|
+
results = []
|
|
284
|
+
query_lower = query.lower()
|
|
285
|
+
|
|
286
|
+
# Search in cached symbols first
|
|
287
|
+
for symbol in self._symbols.values():
|
|
288
|
+
if library and symbol.library != library:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Check if query matches name, description, or keywords
|
|
292
|
+
searchable_text = f"{symbol.name} {symbol.description} {symbol.keywords}".lower()
|
|
293
|
+
if query_lower in searchable_text:
|
|
294
|
+
results.append(symbol)
|
|
295
|
+
if len(results) >= limit:
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
# If not enough results and query looks like a specific symbol, try loading
|
|
299
|
+
if len(results) < 5 and ":" in query:
|
|
300
|
+
symbol = self.get_symbol(query)
|
|
301
|
+
if symbol and symbol not in results:
|
|
302
|
+
results.insert(0, symbol) # Put exact match first
|
|
303
|
+
|
|
304
|
+
return results
|
|
305
|
+
|
|
306
|
+
def get_library_symbols(self, library_name: str) -> List[SymbolDefinition]:
|
|
307
|
+
"""Get all symbols from a specific library."""
|
|
308
|
+
if library_name not in self._library_index:
|
|
309
|
+
logger.warning(f"Library not found: {library_name}")
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
# Load library if not already cached
|
|
313
|
+
library_path = self._library_index[library_name]
|
|
314
|
+
self._load_library(library_path)
|
|
315
|
+
|
|
316
|
+
# Return all symbols from this library
|
|
317
|
+
return [symbol for symbol in self._symbols.values() if symbol.library == library_name]
|
|
318
|
+
|
|
319
|
+
def get_performance_stats(self) -> Dict[str, Any]:
|
|
320
|
+
"""Get cache performance statistics."""
|
|
321
|
+
total_requests = self._cache_hits + self._cache_misses
|
|
322
|
+
hit_rate = (self._cache_hits / total_requests * 100) if total_requests > 0 else 0
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"cache_hits": self._cache_hits,
|
|
326
|
+
"cache_misses": self._cache_misses,
|
|
327
|
+
"hit_rate_percent": round(hit_rate, 2),
|
|
328
|
+
"total_symbols_cached": len(self._symbols),
|
|
329
|
+
"total_libraries": len(self._library_paths),
|
|
330
|
+
"total_load_time_ms": round(self._total_load_time * 1000, 2),
|
|
331
|
+
"avg_load_time_per_symbol_ms": round(
|
|
332
|
+
(self._total_load_time / len(self._symbols) * 1000) if self._symbols else 0, 2
|
|
333
|
+
),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def clear_cache(self):
|
|
337
|
+
"""Clear all cached symbol data."""
|
|
338
|
+
self._symbols.clear()
|
|
339
|
+
self._symbol_index.clear()
|
|
340
|
+
self._cache_hits = 0
|
|
341
|
+
self._cache_misses = 0
|
|
342
|
+
self._total_load_time = 0.0
|
|
343
|
+
logger.info("Symbol cache cleared")
|
|
344
|
+
|
|
345
|
+
def _load_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
|
|
346
|
+
"""Load a single symbol from its library."""
|
|
347
|
+
if ":" not in lib_id:
|
|
348
|
+
logger.warning(f"Invalid lib_id format: {lib_id}")
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
library_name, symbol_name = lib_id.split(":", 1)
|
|
352
|
+
|
|
353
|
+
if library_name not in self._library_index:
|
|
354
|
+
logger.warning(f"Library not found: {library_name}")
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
library_path = self._library_index[library_name]
|
|
358
|
+
return self._load_symbol_from_library(library_path, lib_id)
|
|
359
|
+
|
|
360
|
+
def _load_symbol_from_library(
|
|
361
|
+
self, library_path: Path, lib_id: str
|
|
362
|
+
) -> Optional[SymbolDefinition]:
|
|
363
|
+
"""Load a specific symbol from a library file."""
|
|
364
|
+
start_time = time.time()
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
# This is a simplified version - in reality, you'd parse the .kicad_sym file
|
|
368
|
+
# For now, create a basic symbol definition
|
|
369
|
+
library_name, symbol_name = lib_id.split(":", 1)
|
|
370
|
+
|
|
371
|
+
# Create basic symbol definition
|
|
372
|
+
# In a real implementation, this would parse the actual .kicad_sym file
|
|
373
|
+
symbol = SymbolDefinition(
|
|
374
|
+
lib_id=lib_id,
|
|
375
|
+
name=symbol_name,
|
|
376
|
+
library=library_name,
|
|
377
|
+
reference_prefix=self._guess_reference_prefix(symbol_name),
|
|
378
|
+
description=f"{symbol_name} component",
|
|
379
|
+
pins=[], # Would be loaded from actual file
|
|
380
|
+
load_time=time.time() - start_time,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
self._symbols[lib_id] = symbol
|
|
384
|
+
self._symbol_index[symbol_name] = lib_id
|
|
385
|
+
self._total_load_time += symbol.load_time
|
|
386
|
+
|
|
387
|
+
logger.debug(f"Loaded symbol {lib_id} in {symbol.load_time:.3f}s")
|
|
388
|
+
return symbol
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.error(f"Error loading symbol {lib_id} from {library_path}: {e}")
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
def _load_library(self, library_path: Path) -> bool:
|
|
395
|
+
"""Load all symbols from a library file."""
|
|
396
|
+
library_name = library_path.stem
|
|
397
|
+
|
|
398
|
+
# Check if library needs reloading based on modification time
|
|
399
|
+
if library_name in self._lib_stats:
|
|
400
|
+
stat = library_path.stat()
|
|
401
|
+
if stat.st_mtime <= self._lib_stats[library_name].last_modified:
|
|
402
|
+
logger.debug(f"Library {library_name} already up-to-date")
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
start_time = time.time()
|
|
406
|
+
logger.info(f"Loading library: {library_name}")
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
# In a real implementation, this would parse the .kicad_sym file
|
|
410
|
+
# and extract all symbol definitions
|
|
411
|
+
|
|
412
|
+
# For now, just update statistics
|
|
413
|
+
load_time = time.time() - start_time
|
|
414
|
+
|
|
415
|
+
if library_name not in self._lib_stats:
|
|
416
|
+
stat = library_path.stat()
|
|
417
|
+
self._lib_stats[library_name] = LibraryStats(
|
|
418
|
+
library_path=library_path, file_size=stat.st_size, last_modified=stat.st_mtime
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
self._lib_stats[library_name].load_time = load_time
|
|
422
|
+
self._total_load_time += load_time
|
|
423
|
+
|
|
424
|
+
logger.info(f"Loaded library {library_name} in {load_time:.3f}s")
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.error(f"Error loading library {library_path}: {e}")
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def _guess_reference_prefix(self, symbol_name: str) -> str:
|
|
432
|
+
"""Guess the reference prefix from symbol name."""
|
|
433
|
+
# Common mappings
|
|
434
|
+
prefix_mapping = {
|
|
435
|
+
"R": "R", # Resistor
|
|
436
|
+
"C": "C", # Capacitor
|
|
437
|
+
"L": "L", # Inductor
|
|
438
|
+
"D": "D", # Diode
|
|
439
|
+
"LED": "D", # LED
|
|
440
|
+
"Q": "Q", # Transistor
|
|
441
|
+
"U": "U", # IC
|
|
442
|
+
"J": "J", # Connector
|
|
443
|
+
"SW": "SW", # Switch
|
|
444
|
+
"TP": "TP", # Test point
|
|
445
|
+
"FB": "FB", # Ferrite bead
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
symbol_upper = symbol_name.upper()
|
|
449
|
+
for key, prefix in prefix_mapping.items():
|
|
450
|
+
if symbol_upper.startswith(key):
|
|
451
|
+
return prefix
|
|
452
|
+
|
|
453
|
+
# Default to 'U' for unknown symbols
|
|
454
|
+
return "U"
|
|
455
|
+
|
|
456
|
+
def _get_default_library_paths(self) -> List[Path]:
|
|
457
|
+
"""Get default KiCAD library search paths."""
|
|
458
|
+
search_paths = []
|
|
459
|
+
|
|
460
|
+
# Common KiCAD installation paths
|
|
461
|
+
if os.name == "nt": # Windows
|
|
462
|
+
search_paths.extend(
|
|
463
|
+
[
|
|
464
|
+
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
|
|
465
|
+
Path("C:/Program Files (x86)/KiCad/9.0/share/kicad/symbols"),
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
elif os.name == "posix": # Linux/Mac
|
|
469
|
+
search_paths.extend(
|
|
470
|
+
[
|
|
471
|
+
Path("/usr/share/kicad/symbols"),
|
|
472
|
+
Path("/usr/local/share/kicad/symbols"),
|
|
473
|
+
Path.home() / ".local/share/kicad/symbols",
|
|
474
|
+
]
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# User documents
|
|
478
|
+
search_paths.extend(
|
|
479
|
+
[
|
|
480
|
+
Path.home() / "Documents/KiCad/symbols",
|
|
481
|
+
Path.home() / "kicad/symbols",
|
|
482
|
+
]
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return [path for path in search_paths if path.exists()]
|
|
486
|
+
|
|
487
|
+
def _load_persistent_index(self):
|
|
488
|
+
"""Load persistent symbol index from disk."""
|
|
489
|
+
if not self._enable_persistence or not self._index_file or not self._index_file.exists():
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
with open(self._index_file, "r") as f:
|
|
494
|
+
index_data = json.load(f)
|
|
495
|
+
|
|
496
|
+
# Restore basic index data
|
|
497
|
+
self._symbol_index = index_data.get("symbol_index", {})
|
|
498
|
+
|
|
499
|
+
# Restore library paths
|
|
500
|
+
for lib_path_str in index_data.get("library_paths", []):
|
|
501
|
+
lib_path = Path(lib_path_str)
|
|
502
|
+
if lib_path.exists():
|
|
503
|
+
self.add_library_path(lib_path)
|
|
504
|
+
|
|
505
|
+
logger.info(f"Loaded persistent index with {len(self._symbol_index)} symbols")
|
|
506
|
+
|
|
507
|
+
except Exception as e:
|
|
508
|
+
logger.warning(f"Failed to load persistent index: {e}")
|
|
509
|
+
|
|
510
|
+
def _save_persistent_index(self):
|
|
511
|
+
"""Save symbol index to disk for persistence."""
|
|
512
|
+
if not self._enable_persistence or not self._index_file:
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
index_data = {
|
|
517
|
+
"symbol_index": self._symbol_index,
|
|
518
|
+
"library_paths": [str(path) for path in self._library_paths],
|
|
519
|
+
"cache_stats": self.get_performance_stats(),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
with open(self._index_file, "w") as f:
|
|
523
|
+
json.dump(index_data, f, indent=2)
|
|
524
|
+
|
|
525
|
+
logger.debug("Saved persistent symbol index")
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
logger.warning(f"Failed to save persistent index: {e}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# Global cache instance
|
|
532
|
+
_global_cache: Optional[SymbolLibraryCache] = None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def get_symbol_cache() -> SymbolLibraryCache:
|
|
536
|
+
"""Get the global symbol cache instance."""
|
|
537
|
+
global _global_cache
|
|
538
|
+
if _global_cache is None:
|
|
539
|
+
_global_cache = SymbolLibraryCache()
|
|
540
|
+
# Auto-discover libraries on first use
|
|
541
|
+
_global_cache.discover_libraries()
|
|
542
|
+
return _global_cache
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def set_symbol_cache(cache: SymbolLibraryCache):
|
|
546
|
+
"""Set the global symbol cache instance."""
|
|
547
|
+
global _global_cache
|
|
548
|
+
_global_cache = cache
|