Add hot-update boot pipeline, UI bundles/atlas assets, and design docs.
Ignore AIClient/Library and AIClient/HybridCLRData permanently; include HybridCLR/YooAsset project setup and UI tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Slice UI Normal atlas into Common single sprites."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
ATLAS = Path(r"d:\UD\AI\AIGame\Tools\output\ui_normal_atlas_gpt.png")
|
||||
OUT_DIR = Path(r"d:\UD\AI\AIGame\AIClient\Assets\Bundles\UI\Texture\Common")
|
||||
REPORT = Path(r"d:\UD\AI\AIGame\Tools\output\ui_normal_slice_report.json")
|
||||
|
||||
# From Doc/UI_Normal_Set.md (left-top origin)
|
||||
SLICES = [
|
||||
("ui_panel_main", 8, 8, 480, 256),
|
||||
("ui_panel_popup", 496, 8, 360, 240),
|
||||
("ui_panel_content", 864, 8, 320, 200),
|
||||
("ui_panel_header", 1192, 8, 400, 80),
|
||||
("ui_overlay_dim", 1600, 8, 192, 192),
|
||||
("ui_divider_h", 1192, 96, 400, 16),
|
||||
("btn_primary_normal", 8, 280, 224, 88),
|
||||
("btn_primary_pressed", 240, 280, 224, 88),
|
||||
("btn_primary_disabled", 472, 280, 224, 88),
|
||||
("btn_secondary_normal", 704, 280, 224, 88),
|
||||
("btn_secondary_pressed", 936, 280, 224, 88),
|
||||
("btn_small_normal", 1168, 280, 160, 64),
|
||||
("btn_small_pressed", 1336, 280, 160, 64),
|
||||
("btn_close", 1504, 280, 72, 72),
|
||||
("btn_back", 1584, 280, 72, 72),
|
||||
("scrollbar_track_v", 1664, 280, 28, 160),
|
||||
("scrollbar_thumb_v", 1700, 280, 28, 80),
|
||||
("input_bg_normal", 8, 400, 360, 72),
|
||||
("input_bg_focus", 376, 400, 360, 72),
|
||||
("ui_list_item_normal", 744, 400, 320, 72),
|
||||
("ui_list_item_selected", 1072, 400, 320, 72),
|
||||
("tab_active", 1400, 400, 200, 72),
|
||||
("tab_inactive", 1608, 400, 200, 72),
|
||||
("progress_bg", 8, 504, 400, 32),
|
||||
("progress_fill", 416, 504, 400, 32),
|
||||
("slider_track", 824, 504, 320, 28),
|
||||
("slider_fill", 1152, 504, 320, 28),
|
||||
("slider_handle", 1480, 496, 56, 56),
|
||||
("toast_bg", 1544, 496, 280, 80),
|
||||
("toggle_bg_off", 8, 616, 112, 56),
|
||||
("toggle_bg_on", 128, 616, 112, 56),
|
||||
("toggle_knob", 248, 616, 48, 48),
|
||||
("checkbox_off", 304, 616, 56, 56),
|
||||
("checkbox_on", 368, 616, 56, 56),
|
||||
("ico_search", 432, 616, 56, 56),
|
||||
("ico_check", 496, 616, 56, 56),
|
||||
("ico_settings", 560, 616, 56, 56),
|
||||
("ico_refresh", 624, 616, 56, 56),
|
||||
]
|
||||
|
||||
|
||||
def find_boxes(arr: np.ndarray, alpha_thr: int = 24, min_px: int = 60):
|
||||
mask = arr[:, :, 3] > alpha_thr
|
||||
h, w = mask.shape
|
||||
visited = np.zeros_like(mask, dtype=bool)
|
||||
boxes = []
|
||||
for y in range(h):
|
||||
for x in np.where(mask[y] & ~visited[y])[0]:
|
||||
if visited[y, x] or not mask[y, x]:
|
||||
continue
|
||||
q = deque([(int(y), int(x))])
|
||||
visited[y, x] = True
|
||||
minx = maxx = int(x)
|
||||
miny = maxy = int(y)
|
||||
cnt = 0
|
||||
while q:
|
||||
cy, cx = q.popleft()
|
||||
cnt += 1
|
||||
for ny, nx in ((cy - 1, cx), (cy + 1, cx), (cy, cx - 1), (cy, cx + 1)):
|
||||
if 0 <= ny < h and 0 <= nx < w and mask[ny, nx] and not visited[ny, nx]:
|
||||
visited[ny, nx] = True
|
||||
q.append((ny, nx))
|
||||
minx = min(minx, nx)
|
||||
maxx = max(maxx, nx)
|
||||
miny = min(miny, ny)
|
||||
maxy = max(maxy, ny)
|
||||
bw, bh = maxx - minx + 1, maxy - miny + 1
|
||||
if cnt >= min_px and bw >= 10 and bh >= 8:
|
||||
boxes.append(
|
||||
{
|
||||
"miny": miny,
|
||||
"minx": minx,
|
||||
"maxy": maxy,
|
||||
"maxx": maxx,
|
||||
"w": bw,
|
||||
"h": bh,
|
||||
"cx": (minx + maxx) / 2,
|
||||
"cy": (miny + maxy) / 2,
|
||||
"px": cnt,
|
||||
}
|
||||
)
|
||||
return boxes
|
||||
|
||||
|
||||
def match_slices(boxes: list[dict]) -> list[dict]:
|
||||
used = set()
|
||||
matches = []
|
||||
for name, x, y, w, h in SLICES:
|
||||
pcx, pcy = x + w / 2, y + h / 2
|
||||
best_i, best_score = None, 1e18
|
||||
for i, b in enumerate(boxes):
|
||||
if i in used:
|
||||
continue
|
||||
# prefer position; lightly penalize size mismatch
|
||||
dist = (b["cx"] - pcx) ** 2 + (b["cy"] - pcy) ** 2
|
||||
size_pen = ((b["w"] - w) / max(w, 1)) ** 2 + ((b["h"] - h) / max(h, 1)) ** 2
|
||||
score = dist + size_pen * 8000
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best_i = i
|
||||
if best_i is None:
|
||||
raise RuntimeError(f"no box for {name}")
|
||||
used.add(best_i)
|
||||
b = boxes[best_i]
|
||||
matches.append(
|
||||
{
|
||||
"name": name,
|
||||
"planned": [x, y, w, h],
|
||||
"box": [b["minx"], b["miny"], b["w"], b["h"]],
|
||||
"score": round(best_score, 1),
|
||||
}
|
||||
)
|
||||
return matches
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
img = Image.open(ATLAS).convert("RGBA")
|
||||
arr = np.array(img)
|
||||
boxes = find_boxes(arr)
|
||||
if len(boxes) != len(SLICES):
|
||||
raise SystemExit(f"expected {len(SLICES)} components, got {len(boxes)}")
|
||||
|
||||
matches = match_slices(boxes)
|
||||
written = []
|
||||
for m in matches:
|
||||
x, y, w, h = m["box"]
|
||||
crop = img.crop((x, y, x + w, y + h))
|
||||
# pad 2px transparent to reduce bleed when bilinear sampling
|
||||
pad = 2
|
||||
canvas = Image.new("RGBA", (w + pad * 2, h + pad * 2), (0, 0, 0, 0))
|
||||
canvas.paste(crop, (pad, pad))
|
||||
out = OUT_DIR / f"{m['name']}.png"
|
||||
canvas.save(out)
|
||||
written.append({"name": m["name"], "path": str(out), **m})
|
||||
|
||||
REPORT.write_text(json.dumps({"count": len(written), "items": written}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"sliced {len(written)} sprites -> {OUT_DIR}")
|
||||
print(f"report -> {REPORT}")
|
||||
for m in matches[:5]:
|
||||
print(m["name"], "planned", m["planned"], "->", m["box"], "score", m["score"])
|
||||
print("...")
|
||||
for m in matches[-3:]:
|
||||
print(m["name"], "planned", m["planned"], "->", m["box"], "score", m["score"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user