Files
Shuery 3f11426964 feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源
把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。

- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
  (.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
  抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)

同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
  且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
  目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)

pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
2026-10-02 01:27:02 +08:00

77 lines
2.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Spine atlas 文本的解析与页名规范化。
.atlas 是文本格式:**第一行是贴图页文件名**,接着 `size:` / `filter:` / `format:` / `scale:`
等页面头,之后才是各区域的 `bounds` / `offsets` / `rotate`。换页就是再来一行页文件名。
两种书写风格都要吃(同一批页面里都存在):
size:498,330 ← 紧凑式(nico-tea / zhidong-wonder / kv45)
size: 256, 256 ← 带空格式(get-memory)
抓取期我们只关心两件事:**这具骨架需要哪些贴图页**,以及**页名能不能原样落盘**
(运行时按第一行去请求贴图页,所以文件名必须与第一行逐字一致)。
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
__all__ = ["AtlasInfo", "parse", "page_names", "retarget_pages"]
_PAGE_EXT = re.compile(r"\.(png|webp|jpg|jpeg)$", re.IGNORECASE)
_PAGE_HEAD = re.compile(r"^(size|format|filter|repeat|pma|scale)\s*:", re.IGNORECASE)
_REGION_ATTR = re.compile(
r"^(bounds|offsets|rotate|xy|orig|index|split|pad|width|height)\s*:", re.IGNORECASE
)
@dataclass
class AtlasInfo:
"""一具骨架的 atlas 摘要。"""
pages: list[str] = field(default_factory=list)
regions: list[str] = field(default_factory=list)
region_pages: list[str | None] = field(default_factory=list)
@property
def multi_page(self) -> bool:
return len(self.pages) > 1
def parse(text: str) -> AtlasInfo:
"""解析 atlas 文本。无法识别的行按区域名处理(atlas 里非属性行就是区域名)。"""
info = AtlasInfo()
current: str | None = None
for raw in text.splitlines():
line = raw.strip()
if not line:
current = None
continue
if ":" not in line and _PAGE_EXT.search(line):
current = line
info.pages.append(line)
continue
if _PAGE_HEAD.match(line) or _REGION_ATTR.match(line):
continue
info.regions.append(line)
info.region_pages.append(current)
return info
def page_names(text: str) -> list[str]:
"""只要贴图页名(顺序即 atlas 里声明的顺序)。"""
return parse(text).pages
def retarget_pages(text: str, mapping: dict[str, str]) -> str:
"""把 atlas 里的页名按 mapping 改写(只改整行的页名行,不碰区域名)。"""
out: list[str] = []
for raw in text.splitlines():
line = raw.strip()
if line and ":" not in line and _PAGE_EXT.search(line) and line in mapping:
out.append(mapping[line])
else:
out.append(raw)
tail = "\n" if text.endswith("\n") else ""
return "\n".join(out) + tail