Files
workbuddy_skills/oil-motion/scripts/create_explainer.py
T
admin 237a09a5b0 修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)
一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
2026-10-08 22:29:52 +08:00

204 lines
6.6 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""从通用模板生成独立的动画原理展示页。"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
SKILL_ROOT = Path(__file__).resolve().parent.parent
TEMPLATE_PATH = SKILL_ROOT / "assets" / "motion-explainer-template.html"
CONFIG_PLACEHOLDER = "__MOTION_EXPLAINER_CONFIG__"
DRIVERS = ("pointer-angle", "pointer-x", "drag", "scroll", "autoplay")
PLAY_MODES = ("loop", "pingpong", "once")
def positive_int(value: str) -> int:
number = int(value)
if number <= 0:
raise argparse.ArgumentTypeError("必须是正整数")
return number
def positive_float(value: str) -> float:
number = float(value)
if number <= 0:
raise argparse.ArgumentTypeError("必须大于 0")
return number
def load_manifest(path: Path | None) -> dict[str, Any]:
if path is None:
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise SystemExit(f"无法读取 manifest:{error}") from error
if not isinstance(data, dict):
raise SystemExit("manifest 顶层必须是 JSON 对象")
return data
def first_value(
explicit: Any,
manifest: dict[str, Any],
*keys: str,
default: Any = None,
) -> Any:
if explicit is not None:
return explicit
for key in keys:
if key in manifest and manifest[key] is not None:
return manifest[key]
return default
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="生成带雪碧图、母版视频和实时帧高亮的独立 HTML 展示页。"
)
parser.add_argument("--title", default="一张图,很多种运动。")
parser.add_argument(
"--atlas-url",
required=True,
help="HTML 中使用的图集 URL;相对地址以输出 HTML 所在目录为基准。",
)
parser.add_argument(
"--video-url",
help="可选母版视频 URL;不传时隐藏视频 Tab。",
)
parser.add_argument("--manifest", type=Path, help="可选本地图集 manifest。")
parser.add_argument("--frames", type=positive_int)
parser.add_argument("--columns", type=positive_int)
parser.add_argument("--rows", type=positive_int)
parser.add_argument("--cell-width", type=positive_int)
parser.add_argument("--cell-height", type=positive_int)
parser.add_argument("--rest-frame", type=int)
parser.add_argument("--driver", choices=DRIVERS, default="pointer-angle")
parser.add_argument("--angle-start-deg", type=float)
parser.add_argument("--smooth-time", type=positive_float, default=0.11)
parser.add_argument("--max-frame-speed", type=positive_float)
parser.add_argument("--autoplay-fps", type=positive_float, default=24)
parser.add_argument(
"--autoplay-mode",
choices=PLAY_MODES,
default="loop",
)
parser.add_argument("--scroll-pages", type=positive_float, default=3)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--force", action="store_true")
return parser.parse_args()
def build_config(args: argparse.Namespace) -> dict[str, Any]:
manifest = load_manifest(args.manifest)
frame_count = int(
first_value(args.frames, manifest, "frameCount", "frames", default=0)
)
columns = int(first_value(args.columns, manifest, "columns", default=0))
rows = int(first_value(args.rows, manifest, "rows", default=0))
cell_width = int(
first_value(args.cell_width, manifest, "cellWidth", default=1)
)
cell_height = int(
first_value(args.cell_height, manifest, "cellHeight", default=1)
)
if frame_count <= 0 or columns <= 0 or rows <= 0:
raise SystemExit(
"缺少图集尺寸:请提供 --frames、--columns、--rows,或传入包含这些字段的 manifest。"
)
if columns * rows < frame_count:
raise SystemExit(
f"图集网格只有 {columns * rows} 个格子,无法容纳 {frame_count} 帧。"
)
if cell_width <= 0 or cell_height <= 0:
raise SystemExit("cellWidth 和 cellHeight 必须大于 0。")
rest_frame = int(
first_value(
args.rest_frame,
manifest,
"initialFrame",
"restFrame",
default=0,
)
)
if not 0 <= rest_frame < frame_count:
raise SystemExit(
f"rest frame 必须位于 0 到 {frame_count - 1} 之间。"
)
manifest_angle = manifest.get("startAngleRadians")
if args.angle_start_deg is not None:
angle_start_deg = args.angle_start_deg
elif manifest_angle is not None:
angle_start_deg = math.degrees(float(manifest_angle))
else:
angle_start_deg = -135
max_frame_speed = (
args.max_frame_speed
if args.max_frame_speed is not None
else max(60.0, frame_count * 2.0)
)
return {
"title": args.title,
"atlasUrl": args.atlas_url,
"videoUrl": args.video_url or "",
"frameCount": frame_count,
"columns": columns,
"rows": rows,
"cellWidth": cell_width,
"cellHeight": cell_height,
"restFrame": rest_frame,
"driver": args.driver,
"angleStartDeg": angle_start_deg,
"smoothTime": args.smooth_time,
"maxFrameSpeed": max_frame_speed,
"autoplayFps": args.autoplay_fps,
"autoplayMode": args.autoplay_mode,
"scrollPages": args.scroll_pages,
}
def main() -> None:
args = parse_args()
if args.output.exists() and not args.force:
raise SystemExit(
f"输出已存在:{args.output}。确认后使用 --force 覆盖。"
)
if not TEMPLATE_PATH.exists():
raise SystemExit(f"找不到模板:{TEMPLATE_PATH}")
template = TEMPLATE_PATH.read_text(encoding="utf-8")
if template.count(CONFIG_PLACEHOLDER) != 1:
raise SystemExit("模板配置占位符缺失或不唯一。")
config = build_config(args)
embedded = json.dumps(
config,
ensure_ascii=False,
separators=(",", ":"),
).replace("</", "<\\/")
output = template.replace(CONFIG_PLACEHOLDER, embedded)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(output, encoding="utf-8")
print(f"已生成:{args.output.resolve()}")
print(
"配置:"
f"{config['frameCount']} 帧,"
f"{config['columns']}×{config['rows']},"
f"driver={config['driver']}"
)
if __name__ == "__main__":
main()