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 处引用自包含。
This commit is contained in:
1 parent
b8eee05d78
commit
3f11426964
297 files changed
+216627
-1926
No files matched your search
@@ -0,0 +1,76 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user