修: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` 挪回原处即可。
This commit is contained in:
admin committed 2026-10-08 22:29:52 +08:00
1 parent e03465c398
commit 237a09a5b0
161 files changed
+19429 -2

No files matched your search

+203
View File
@@ -0,0 +1,203 @@
#!/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()