231 lines
7.6 KiB
Python
231 lines
7.6 KiB
Python
"""Default designator type names (singular/plural) by letter prefix."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
# Minimal useful defaults; projects can override via DB
|
|
DEFAULT_MAPPINGS: dict[str, tuple[str, str]] = {
|
|
"C": ("Конденсатор", "Конденсаторы"),
|
|
"R": ("Резистор", "Резисторы"),
|
|
"L": ("Катушка индуктивности", "Катушки индуктивности"),
|
|
"D": ("Диод", "Диоды"),
|
|
"VD": ("Диод", "Диоды"),
|
|
"VT": ("Транзистор", "Транзисторы"),
|
|
"Q": ("Транзистор", "Транзисторы"),
|
|
"DA": ("Микросхема", "Микросхемы"),
|
|
"DD": ("Микросхема", "Микросхемы"),
|
|
"U": ("Микросхема", "Микросхемы"),
|
|
"X": ("Соединитель", "Соединители"),
|
|
"XP": ("Соединитель", "Соединители"),
|
|
"XS": ("Соединитель", "Соединители"),
|
|
"FU": ("Предохранитель", "Предохранители"),
|
|
"F": ("Предохранитель", "Предохранители"),
|
|
"SA": ("Переключатель", "Переключатели"),
|
|
"SB": ("Кнопка", "Кнопки"),
|
|
"HL": ("Индикатор", "Индикаторы"),
|
|
"HG": ("Индикатор", "Индикаторы"),
|
|
"G": ("Генератор", "Генераторы"),
|
|
"T": ("Трансформатор", "Трансформаторы"),
|
|
"TV": ("Трансформатор", "Трансформаторы"),
|
|
"K": ("Реле", "Реле"),
|
|
"B": ("Пьезоэлемент", "Пьезоэлементы"),
|
|
"Z": ("Фильтр", "Фильтры"),
|
|
}
|
|
|
|
|
|
def split_designator(designator: str) -> tuple[str, str]:
|
|
m = re.match(r"^([A-Za-z]+)(\d*)$", designator.strip())
|
|
if m:
|
|
return m.group(1), m.group(2)
|
|
for i, ch in enumerate(designator):
|
|
if ch.isdigit():
|
|
return designator[:i], designator[i:]
|
|
return designator, ""
|
|
|
|
|
|
def designator_sort_key(designator: str) -> tuple[str, int, str]:
|
|
letter, num = split_designator(designator)
|
|
try:
|
|
n = int(num) if num else 0
|
|
except ValueError:
|
|
n = 0
|
|
return letter, n, num
|
|
return letter.upper(), n, num
|
|
|
|
|
|
def prefix_from_designator(designator: str) -> str:
|
|
letter, _ = split_designator(designator)
|
|
return letter.upper()
|
|
|
|
|
|
def get_type_names(
|
|
designator: str, mappings: dict[str, tuple[str, str]] | None = None
|
|
) -> tuple[str, str]:
|
|
maps = {**DEFAULT_MAPPINGS, **(mappings or {})}
|
|
prefix = prefix_from_designator(designator)
|
|
if prefix in maps:
|
|
return maps[prefix]
|
|
# try 2-letter then 1-letter
|
|
if len(prefix) >= 2 and prefix[:2] in maps:
|
|
return maps[prefix[:2]]
|
|
if prefix[:1] in maps:
|
|
return maps[prefix[:1]]
|
|
return (prefix, prefix)
|
|
|
|
|
|
def format_designator_range(designators: list[str]) -> str:
|
|
if not designators:
|
|
return ""
|
|
if len(designators) == 1:
|
|
return designators[0]
|
|
if len(designators) == 2:
|
|
return f"{designators[0]}, {designators[1]}"
|
|
return f"{designators[0]}-{designators[-1]}"
|
|
|
|
|
|
def format_consecutive_ranges(designators: list[str]) -> str:
|
|
"""Group consecutive numbered designators into ranges."""
|
|
if not designators:
|
|
return ""
|
|
parsed = []
|
|
for d in designators:
|
|
letter, num = split_designator(d)
|
|
try:
|
|
n = int(num) if num else None
|
|
except ValueError:
|
|
n = None
|
|
parsed.append((d, letter, n))
|
|
|
|
segments: list[list[str]] = []
|
|
current: list[str] = []
|
|
last_letter = None
|
|
last_num = None
|
|
for d, letter, n in parsed:
|
|
if not current:
|
|
current = [d]
|
|
last_letter, last_num = letter, n
|
|
continue
|
|
if letter == last_letter and n is not None and last_num is not None and n == last_num + 1:
|
|
current.append(d)
|
|
last_num = n
|
|
else:
|
|
segments.append(current)
|
|
current = [d]
|
|
last_letter, last_num = letter, n
|
|
if current:
|
|
segments.append(current)
|
|
|
|
parts = []
|
|
for seg in segments:
|
|
if len(seg) == 1:
|
|
parts.append(seg[0])
|
|
elif len(seg) == 2:
|
|
parts.append(f"{seg[0]}, {seg[1]}")
|
|
else:
|
|
parts.append(f"{seg[0]}-{seg[-1]}")
|
|
return ", ".join(parts)
|
|
|
|
|
|
def split_long_text(text: str, max_length: int) -> list[str]:
|
|
if not text or len(text) <= max_length:
|
|
return [text] if text is not None else [""]
|
|
parts: list[str] = []
|
|
remaining = text
|
|
while len(remaining) > max_length:
|
|
chunk = remaining[:max_length]
|
|
split_at = chunk.rfind(",")
|
|
if split_at < max_length // 3:
|
|
split_at = chunk.rfind(" ")
|
|
if split_at < max_length // 3:
|
|
split_at = max_length
|
|
parts.append(remaining[:split_at].rstrip(", ").strip())
|
|
remaining = remaining[split_at:].lstrip(", ").strip()
|
|
if remaining:
|
|
parts.append(remaining)
|
|
return parts or [""]
|
|
|
|
|
|
def _split_expression_parts(expr: str) -> list[str]:
|
|
"""Split Altium expression by + outside quotes."""
|
|
parts: list[str] = []
|
|
current: list[str] = []
|
|
in_double = False
|
|
in_single = False
|
|
for ch in expr:
|
|
if ch == '"' and not in_single:
|
|
in_double = not in_double
|
|
current.append(ch)
|
|
elif ch == "'" and not in_double:
|
|
in_single = not in_single
|
|
current.append(ch)
|
|
elif ch == "+" and not in_double and not in_single:
|
|
if current:
|
|
parts.append("".join(current).strip())
|
|
current = []
|
|
else:
|
|
current.append(ch)
|
|
if current:
|
|
parts.append("".join(current).strip())
|
|
return parts
|
|
|
|
|
|
def _resolve_property(
|
|
name: str,
|
|
props: dict[str, str],
|
|
designator: str = "",
|
|
resolving: frozenset[str] | None = None,
|
|
) -> str:
|
|
if resolving is None:
|
|
resolving = frozenset()
|
|
if name in resolving:
|
|
return ""
|
|
if name == "Designator":
|
|
raw = designator or props.get("Designator", "")
|
|
else:
|
|
raw = props.get(name, "")
|
|
if not raw:
|
|
return ""
|
|
if raw.startswith("="):
|
|
return resolve_expression(raw, props, designator, resolving | {name})
|
|
return raw
|
|
|
|
|
|
def resolve_expression(
|
|
value: str,
|
|
props: dict[str, str],
|
|
designator: str = "",
|
|
resolving: frozenset[str] | None = None,
|
|
) -> str:
|
|
"""
|
|
Altium complex string: ='literal'+"Property"+Name
|
|
- single quotes = literal text
|
|
- double quotes = component property reference
|
|
- bare identifiers = property reference
|
|
"""
|
|
if resolving is None:
|
|
resolving = frozenset()
|
|
if len(resolving) > 64:
|
|
return ""
|
|
if not value:
|
|
return ""
|
|
value = value.strip()
|
|
if value.startswith('"') and value.endswith('"') and not value.startswith('="'):
|
|
return _resolve_property(value[1:-1], props, designator, resolving)
|
|
if not value.startswith("="):
|
|
return value
|
|
|
|
result: list[str] = []
|
|
for part in _split_expression_parts(value[1:]):
|
|
if not part:
|
|
continue
|
|
if part.startswith("'") and part.endswith("'") and len(part) >= 2:
|
|
result.append(part[1:-1])
|
|
elif part.startswith('"') and part.endswith('"') and len(part) >= 2:
|
|
result.append(_resolve_property(part[1:-1], props, designator, resolving))
|
|
elif part == "Designator":
|
|
result.append(designator or props.get("Designator", ""))
|
|
else:
|
|
result.append(_resolve_property(part, props, designator, resolving))
|
|
return "".join(result)
|