oil-ui-pro 0.15.4 → 0.22.2(上游 ui.oiloil.org 正式包)+ 记 P0-98 跨盘 rename 坑
- oil-ui-pro 升到 0.22.2:references 13→28 份(新增 16、删 imagery.md)、assets 增 cards/(22 组)与 fonts/、SKILL.md 顶层五步结构未改名。 - 升级路径:官方 `npx github:oil-oil/oil-cli update` 稳定失败(error=replace、报「检查目录权限」)。 真身为 EXDEV —— 解压在 os.tmpdir()(C 盘)、技能目录在 E 盘,Windows 上 rename 不能跨盘; CLI 的 catch 把非 CliError 原生异常一律兜底成 replaceFailed,真错误一个字不吐。 做法=借其 prepare() 只做下载+校验,再 cp 到同盘 stage、rename 交换;官方命令复验 up_to_date。 - 回归:product-planning 按名引用的 visual-review.md/tools.md/design-direction.md/style-explorer.md 全部健在;命名闸门 2061|0|27。 - session-mechanism/references/pitfalls.md 新增 P0-98(含通则:更新器把原生错误兜底成指错方向的人话)。
This commit is contained in:
1 parent
053a6a6723
commit
d9445b5888
126 files changed
+10345
-1401
No files matched your search
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build an offline gallery, including a 360x180 Natural Earth land bitmap.
|
||||
|
||||
Python 3.10+, standard library only. No network and no JS dependencies.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CARDS = ROOT / 'assets/cards'
|
||||
NAMES = ['particle-ring', 'flow-object', 'dot-globe', 'line-ribbons', 'showcase',
|
||||
'ui-slice', 'terminal', 'metric', 'beam-graph', 'keycaps', 'promo-band',
|
||||
'vector-space', 'file-mosaic', 'stream-window', 'portrait-story', 'capture-notes',
|
||||
'answer-lens', 'responsive-preview', 'deploy-preview']
|
||||
|
||||
|
||||
def land_mask() -> None:
|
||||
with zipfile.ZipFile(CARDS / 'shared/ne_110m_land.zip') as archive:
|
||||
data = archive.read('ne_110m_land.shp')
|
||||
polygons = []
|
||||
offset = 100
|
||||
while offset < len(data):
|
||||
length = struct.unpack_from('>i', data, offset + 4)[0] * 2
|
||||
record = data[offset + 8:offset + 8 + length]
|
||||
offset += 8 + length
|
||||
if struct.unpack_from('<i', record)[0] != 5:
|
||||
continue
|
||||
parts, count = struct.unpack_from('<ii', record, 36)
|
||||
starts = list(struct.unpack_from(f'<{parts}i', record, 44)) + [count]
|
||||
coords = list(struct.iter_unpack('<dd', record[44 + parts * 4:]))
|
||||
polygons.extend(coords[starts[i]:starts[i + 1]] for i in range(parts))
|
||||
width, height = 360, 180
|
||||
bits = bytearray(math.ceil(width * height / 8))
|
||||
# Scanline even/odd fill within each ring. Land data contains exterior rings.
|
||||
for y in range(height):
|
||||
latitude = 90 - (y + .5) * 180 / height
|
||||
for ring in polygons:
|
||||
hits = []
|
||||
for (x1, y1), (x2, y2) in zip(ring, ring[1:] + ring[:1]):
|
||||
if (y1 > latitude) != (y2 > latitude):
|
||||
hits.append(x1 + (latitude - y1) * (x2 - x1) / (y2 - y1))
|
||||
hits.sort()
|
||||
for lo, hi in zip(hits[::2], hits[1::2]):
|
||||
start = max(0, math.ceil((lo + 180) / 360 * width - .5))
|
||||
end = min(width, math.ceil((hi + 180) / 360 * width - .5))
|
||||
for x in range(start, end):
|
||||
i = y * width + x
|
||||
bits[i // 8] |= 1 << (i % 8)
|
||||
encoded = base64.b64encode(bits).decode()
|
||||
(CARDS / 'shared/land-mask.js').write_text(
|
||||
'// Generated from Natural Earth 110m land (public domain).\n'
|
||||
f'export const LAND_WIDTH = {width};\nexport const LAND_HEIGHT = {height};\n'
|
||||
f'export const LAND_BITS = "{encoded}";\n', encoding='utf-8', newline='\n')
|
||||
|
||||
|
||||
def bundle(path: Path) -> str:
|
||||
source = path.read_text(encoding='utf-8')
|
||||
exports = re.findall(r'export\s+(?:const|let|function|class)\s+(\w+)', source)
|
||||
def replace_import(match: re.Match) -> str:
|
||||
target = (path.parent / match[2]).resolve().relative_to(CARDS).as_posix()
|
||||
return f'const {{{match[1]}}} = modules["{target}"];'
|
||||
source = re.sub(r'import\s*\{([^}]+)\}\s*from\s*[\'"]([^\'"]+)[\'"];?', replace_import, source)
|
||||
source = re.sub(r'\bexport\s+(?=const|let|function|class)', '', source)
|
||||
if re.search(r'^\s*(import|export)\b', source, re.M):
|
||||
raise ValueError(f'Unsupported module syntax: {path}')
|
||||
key = path.relative_to(CARDS).as_posix()
|
||||
return f'modules["{key}"] = (() => {{\n{source}\nreturn {{{", ".join(exports)}}};\n}})();'
|
||||
|
||||
|
||||
def utf8_output() -> None:
|
||||
"""管道另一头按 UTF-8 读;控制台的代码页可能连中文都没有。"""
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(stream, 'reconfigure') and not stream.isatty():
|
||||
stream.reconfigure(encoding='utf-8')
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
utf8_output()
|
||||
parser = argparse.ArgumentParser(
|
||||
description='维护卡片库时用:把 assets/cards/ 下各个卡片原型重新打包成 assets/cards/gallery.html。',
|
||||
epilog='不带参数运行。它会直接改写 Skill 目录里的 gallery.html,所以只在改过卡片原型的源文件之后运行;'
|
||||
'做设计时不需要它,直接打开 assets/cards/gallery.html 或读各原型目录里的 card.html、card.css。')
|
||||
parser.parse_args(argv)
|
||||
land_mask()
|
||||
html = (CARDS / 'gallery.template.html').read_text(encoding='utf-8')
|
||||
for name in NAMES:
|
||||
html = html.replace('{{' + name + '}}', (CARDS / name / 'card.html').read_text(encoding='utf-8'))
|
||||
css = '\n'.join((CARDS / p).read_text(encoding='utf-8') for p in
|
||||
['shared/shell.css', 'gallery.css'] + [f'{n}/card.css' for n in NAMES] + ['shared/finish.css'])
|
||||
paths = [CARDS / 'gallery-palette.js', CARDS / 'gallery-catalog.js', CARDS / 'shared/content.js', CARDS / 'shared/runtime.js', CARDS / 'shared/land-mask.js', CARDS / 'line-ribbons/surface.js', CARDS / 'flow-object/material.js'] + [CARDS / n / 'card.js' for n in NAMES]
|
||||
js = '(() => {\nconst modules = {};\n' + '\n'.join(bundle(p) for p in paths)
|
||||
js += '\n' + (CARDS / 'gallery-content.js').read_text(encoding='utf-8') + '\n' + (CARDS / 'gallery.js').read_text(encoding='utf-8') + '\n})();'
|
||||
html = html.replace('{{styles}}', css).replace('{{scripts}}', js.replace('</script', '<\\/script'))
|
||||
# Local image slots are embedded at build time, preserving file:// portability.
|
||||
def embed_image(match: re.Match) -> str:
|
||||
path = (CARDS / match[1]).resolve()
|
||||
path.relative_to(CARDS)
|
||||
mime = {'.jpg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp'}[path.suffix]
|
||||
return 'src="data:' + mime + ';base64,' + base64.b64encode(path.read_bytes()).decode() + '"'
|
||||
html = re.sub(r'src="((?:showcase|portrait-story)/images/[^"<>]+)"', embed_image, html)
|
||||
if re.search(r'\{\{[\w-]+\}\}', html):
|
||||
raise ValueError('Unresolved template slot')
|
||||
(CARDS / 'gallery.html').write_text(html, encoding='utf-8', newline='\n')
|
||||
print(f'Built assets/cards/gallery.html · {len(html.encode()):,} bytes · {len(NAMES)} templates')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user