mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
|
|
CAD_TOKEN_RE = re.compile(r"^\s*@cad\[([^\]#]+)(?:#([^\]]+))?\]")
|
|
OCCURRENCE_SELECTOR_RE = re.compile(r"^o((?:\d+)(?:\.\d+)*)$")
|
|
OCCURRENCE_ENTITY_SELECTOR_RE = re.compile(r"^o((?:\d+)(?:\.\d+)*)\.([sfev])(\d+)$")
|
|
ENTITY_SELECTOR_RE = re.compile(r"^([sfev])(\d+)$")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ParsedSelector:
|
|
selector_type: str
|
|
occurrence_id: str
|
|
ordinal: int | None
|
|
canonical: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ParsedToken:
|
|
line: int
|
|
token: str
|
|
cad_path: str
|
|
selectors: tuple[str, ...]
|
|
|
|
|
|
def _selector_type_for_kind(kind: str) -> str:
|
|
if kind == "s":
|
|
return "shape"
|
|
if kind == "f":
|
|
return "face"
|
|
if kind == "e":
|
|
return "edge"
|
|
return "vertex"
|
|
|
|
|
|
def parse_cad_tokens(text: str) -> list[ParsedToken]:
|
|
tokens: list[ParsedToken] = []
|
|
for line_no, line in enumerate(text.splitlines() or [text], start=1):
|
|
match = CAD_TOKEN_RE.match(line)
|
|
if match is None:
|
|
continue
|
|
cad_path = normalize_cad_path(str(match.group(1) or ""))
|
|
if not cad_path:
|
|
continue
|
|
tokens.append(
|
|
ParsedToken(
|
|
line=line_no,
|
|
token=match.group(0),
|
|
cad_path=cad_path,
|
|
selectors=tuple(normalize_selector_list(str(match.group(2) or ""))),
|
|
)
|
|
)
|
|
return tokens
|
|
|
|
|
|
def normalize_cad_path(raw_cad_path: str) -> str | None:
|
|
normalized = str(raw_cad_path or "").replace("\\", "/").strip().strip("/")
|
|
if not normalized:
|
|
return None
|
|
if normalized.startswith("cad/"):
|
|
normalized = f"models/{normalized[len('cad/') :]}"
|
|
for suffix in (".step", ".stp"):
|
|
if normalized.lower().endswith(suffix):
|
|
normalized = normalized[: -len(suffix)]
|
|
break
|
|
if not normalized.startswith("models/"):
|
|
normalized = f"models/{normalized}"
|
|
parts = normalized.split("/")
|
|
if any(not part or part in {".", ".."} for part in parts):
|
|
return None
|
|
return "/".join(parts)
|
|
|
|
|
|
def parse_selector(raw_selector: str, *, inherited_occurrence_id: str = "") -> ParsedSelector | None:
|
|
selector = str(raw_selector or "").strip().replace("#", "", 1)
|
|
if not selector:
|
|
return None
|
|
|
|
occurrence_entity_match = OCCURRENCE_ENTITY_SELECTOR_RE.match(selector)
|
|
if occurrence_entity_match:
|
|
occurrence_id = f"o{occurrence_entity_match.group(1)}"
|
|
kind = str(occurrence_entity_match.group(2))
|
|
ordinal = int(occurrence_entity_match.group(3))
|
|
return ParsedSelector(
|
|
selector_type=_selector_type_for_kind(kind),
|
|
occurrence_id=occurrence_id,
|
|
ordinal=ordinal,
|
|
canonical=f"{occurrence_id}.{kind}{ordinal}",
|
|
)
|
|
|
|
occurrence_match = OCCURRENCE_SELECTOR_RE.match(selector)
|
|
if occurrence_match:
|
|
occurrence_id = f"o{occurrence_match.group(1)}"
|
|
return ParsedSelector(
|
|
selector_type="occurrence",
|
|
occurrence_id=occurrence_id,
|
|
ordinal=None,
|
|
canonical=occurrence_id,
|
|
)
|
|
|
|
entity_match = ENTITY_SELECTOR_RE.match(selector)
|
|
if entity_match:
|
|
kind = str(entity_match.group(1))
|
|
ordinal = int(entity_match.group(2))
|
|
if inherited_occurrence_id:
|
|
return ParsedSelector(
|
|
selector_type=_selector_type_for_kind(kind),
|
|
occurrence_id=inherited_occurrence_id,
|
|
ordinal=ordinal,
|
|
canonical=f"{inherited_occurrence_id}.{kind}{ordinal}",
|
|
)
|
|
return ParsedSelector(
|
|
selector_type=_selector_type_for_kind(kind),
|
|
occurrence_id="",
|
|
ordinal=ordinal,
|
|
canonical=f"{kind}{ordinal}",
|
|
)
|
|
|
|
return ParsedSelector(
|
|
selector_type="opaque",
|
|
occurrence_id="",
|
|
ordinal=None,
|
|
canonical=selector,
|
|
)
|
|
|
|
|
|
def normalize_selector_list(raw_selector_list: str) -> list[str]:
|
|
normalized: list[str] = []
|
|
inherited_occurrence_id = ""
|
|
for raw_selector in str(raw_selector_list or "").split(","):
|
|
parsed = parse_selector(raw_selector, inherited_occurrence_id=inherited_occurrence_id)
|
|
if parsed is None:
|
|
continue
|
|
normalized.append(parsed.canonical)
|
|
if parsed.occurrence_id:
|
|
inherited_occurrence_id = parsed.occurrence_id
|
|
return normalized
|
|
|
|
|
|
def sort_selectors(selectors: list[str] | tuple[str, ...]) -> list[str]:
|
|
parsed = [parse_selector(selector) for selector in selectors]
|
|
sortable = [(index, item) for index, item in enumerate(parsed) if item is not None]
|
|
rank = {"occurrence": 1, "shape": 2, "face": 3, "edge": 4, "vertex": 5, "opaque": 6}
|
|
sortable.sort(
|
|
key=lambda item: (
|
|
item[1].occurrence_id,
|
|
rank.get(item[1].selector_type, 99),
|
|
item[1].ordinal or 0,
|
|
item[1].canonical,
|
|
item[0],
|
|
)
|
|
)
|
|
return [item[1].canonical for item in sortable]
|
|
|
|
|
|
def build_cad_token(cad_path: str, selector: str = "") -> str:
|
|
normalized_cad_path = normalize_cad_path(cad_path or "")
|
|
if not normalized_cad_path:
|
|
raise ValueError("cad_path is required")
|
|
if not selector:
|
|
return f"@cad[{normalized_cad_path}]"
|
|
return f"@cad[{normalized_cad_path}#{selector}]"
|