Files
ttstd-3d-director/scripts/generate_icon.py
tongtongstudio b9fd289caf chore: 迁移到 pnpm 并更新打包与测试文档
- 用 pnpm 替换 npm,锁定 pnpm@10.33.0,删除 package-lock.json 相关说明
- package.json 将渲染层依赖移入 devDependencies,减小 app.asar 体积
- 新增 electron:smoke 冒烟脚本与 icon 生成脚本
- win 平台设置 AppUserModelID,修复任务栏图标归组
- 清理 tsBuildInfoFile 输出路径至 node_modules/.tmp
- 更新测试断言以匹配画幅遮罩、取景器缩放与模型库外部资产行为
- README 与打包指南补充 pnpm、镜像与常见问题说明
2026-09-16 20:03:57 +08:00

137 lines
4.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.
#!/usr/bin/env python3
"""生成应用图标 public/icon.png(512x512)。
仓库原先引用了 public/icon.png 但文件缺失,导致 electron-builder 回落到默认
Electron 图标。本脚本零第三方依赖(仅标准库 zlib/struct),可重复生成:
python scripts/generate_icon.py
图形语义:视口取景框(四角括号)+ 播放三角,对应"预演/播放分镜"。
"""
from __future__ import annotations
import struct
import zlib
from pathlib import Path
SIZE = 512
SS = 3 # 超采样倍率,用于边缘抗锯齿
W = SIZE * SS
# 取自 src/styles/index.css 的设计令牌
BG_TOP = (22, 24, 28)
BG_BOTTOM = (9, 9, 9)
BRACKET = (242, 244, 246)
ACCENT = (3, 150, 255)
def rounded_rect_coverage(px: float, py: float, x0: float, y0: float, x1: float, y1: float, radius: float) -> bool:
if not (x0 <= px <= x1 and y0 <= py <= y1):
return False
cx = min(max(px, x0 + radius), x1 - radius)
cy = min(max(py, y0 + radius), y1 - radius)
dx = px - cx
dy = py - cy
return dx * dx + dy * dy <= radius * radius
def bracket_coverage(px: float, py: float, inset: float, arm: float, thickness: float) -> bool:
"""四角取景框括号:每角由一条横臂和一条竖臂组成。"""
lo = inset
hi = SIZE - inset
horizontal_arm = (lo <= px <= lo + arm) or (hi - arm <= px <= hi)
vertical_arm = (lo <= py <= lo + arm) or (hi - arm <= py <= hi)
horizontal_bar = (lo <= py <= lo + thickness) or (hi - thickness <= py <= hi)
vertical_bar = (lo <= px <= lo + thickness) or (hi - thickness <= px <= hi)
return (horizontal_arm and horizontal_bar) or (vertical_arm and vertical_bar)
def triangle_coverage(px: float, py: float) -> bool:
ax, ay = 196.0, 168.0
bx, by = 196.0, 344.0
cx, cy = 356.0, 256.0
d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by)
d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy)
d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay)
has_neg = d1 < 0 or d2 < 0 or d3 < 0
has_pos = d1 > 0 or d2 > 0 or d3 > 0
return not (has_neg and has_pos)
def build_pixels() -> bytearray:
samples = SS * SS
pixels = bytearray(SIZE * SIZE * 4)
for oy in range(SIZE):
for ox in range(SIZE):
bg_hits = 0
bracket_hits = 0
triangle_hits = 0
for sy in range(SS):
for sx in range(SS):
px = ox + (sx + 0.5) / SS
py = oy + (sy + 0.5) / SS
if rounded_rect_coverage(px, py, 0.0, 0.0, float(SIZE), float(SIZE), 118.0):
bg_hits += 1
if bracket_coverage(px, py, inset=92.0, arm=104.0, thickness=26.0):
bracket_hits += 1
if triangle_coverage(px, py):
triangle_hits += 1
alpha = bg_hits / samples
if alpha <= 0:
continue
ratio = oy / (SIZE - 1)
base = tuple(
BG_TOP[channel] + (BG_BOTTOM[channel] - BG_TOP[channel]) * ratio for channel in range(3)
)
accent_cov = (bracket_hits + triangle_hits) / samples
if accent_cov > 0:
layer = BRACKET if bracket_hits >= triangle_hits else ACCENT
layer_alpha = min(1.0, accent_cov / max(alpha, 1e-6))
base = tuple(
base[channel] + (layer[channel] - base[channel]) * layer_alpha for channel in range(3)
)
offset = (oy * SIZE + ox) * 4
pixels[offset] = int(round(base[0]))
pixels[offset + 1] = int(round(base[1]))
pixels[offset + 2] = int(round(base[2]))
pixels[offset + 3] = int(round(alpha * 255))
return pixels
def write_png(path: Path, pixels: bytearray) -> None:
raw = bytearray()
stride = SIZE * 4
for y in range(SIZE):
raw.append(0) # filter type 0
raw.extend(pixels[y * stride : (y + 1) * stride])
def chunk(tag: bytes, payload: bytes) -> bytes:
return (
struct.pack(">I", len(payload))
+ tag
+ payload
+ struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(bytes(raw), 9))
png += chunk(b"IEND", b"")
path.write_bytes(png)
def main() -> None:
target = Path(__file__).resolve().parent.parent / "public" / "icon.png"
target.parent.mkdir(parents=True, exist_ok=True)
write_png(target, build_pixels())
print(f"wrote {target} ({target.stat().st_size} bytes, {SIZE}x{SIZE})")
if __name__ == "__main__":
main()