ef0ca8b350
- screen_manager/app/tile_icons.py is the single icon list (12 groups, 20 media and music icons); tools/generate_icons.py writes it into the three icon fonts of both boards and builds the editor's subset font. - Firmware 0.2.18 shows o.icon when its fonts carry the glyph, else the domain icon. The app sends the chosen icon, or Home Assistant's own mdi icon on auto. - Tile sheet gets an icon picker with search; the mockup shows the icon the screen actually draws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
4.2 KiB
Python
103 lines
4.2 KiB
Python
"""Write the tile icon set into both board profiles and build the editor's icon font.
|
|
|
|
The list lives in screen_manager/app/tile_icons.py. Every Material Design Icons
|
|
font of a board profile gets exactly those glyphs (the first font carries the
|
|
list as a YAML anchor, the smaller sizes reuse it), and the editor gets a subset
|
|
of the same TTF so its mockup shows the glyphs the screen draws. Run
|
|
tools/generate_packages.py afterwards; --check changes nothing and fails when
|
|
anything is out of date.
|
|
"""
|
|
import argparse
|
|
import io
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fontTools import subset
|
|
from fontTools.ttLib import TTFont
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
|
import tile_icons # noqa: E402
|
|
|
|
TTF = ROOT / 'fonts/materialdesignicons-webfont.ttf'
|
|
PROFILES = [ROOT / 'home-like-2432s028.yaml', ROOT / 'guition-4848s040.yaml']
|
|
WEB_FONT = ROOT / 'screen_manager/app/static/tile-icons.woff'
|
|
FONT_BLOCK = re.compile(r"( - file: 'fonts/materialdesignicons-webfont\.ttf'\n id: \w+\n size: \d+\n) glyphs:.*\n(?: .*\n)*")
|
|
|
|
def verify():
|
|
"""Names and codepoints must match the TTF the firmware is built from."""
|
|
codepoints = {}
|
|
for code, name in TTFont(TTF).getBestCmap().items():
|
|
codepoints.setdefault(name, code)
|
|
codepoints[name] = min(codepoints[name], code)
|
|
seen = set()
|
|
for name, code in tile_icons.GLYPHS.items():
|
|
if codepoints.get(name) != int(code, 16):
|
|
raise SystemExit(f'{name}: codepoint {code} does not match {TTF.name}')
|
|
if code in seen:
|
|
raise SystemExit(f'Duplicate glyph {code}')
|
|
seen.add(code)
|
|
if len(tile_icons.ICONS) != sum(len(icons) for _, icons in tile_icons.GROUPS):
|
|
raise SystemExit('An icon appears in more than one group')
|
|
|
|
def glyph_list():
|
|
lines = [' glyphs: &tile_icons # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py']
|
|
lines += [f' - "\\U000{code}" # {name}' for name, code in tile_icons.GLYPHS.items()]
|
|
return '\n'.join(lines) + '\n'
|
|
|
|
def profile(text):
|
|
blocks = list(FONT_BLOCK.finditer(text))
|
|
if len(blocks) != 3:
|
|
raise SystemExit(f'Expected three icon fonts, found {len(blocks)}')
|
|
out, last = [], 0
|
|
for index, block in enumerate(blocks):
|
|
out += [text[last:block.start()], block[1], glyph_list() if index == 0 else ' glyphs: *tile_icons\n']
|
|
last = block.end()
|
|
return ''.join(out) + text[last:]
|
|
|
|
def web_font():
|
|
font = TTFont(TTF)
|
|
options = subset.Options()
|
|
options.flavor = 'woff'
|
|
options.layout_features = []
|
|
options.name_IDs = []
|
|
options.notdef_outline = False
|
|
subsetter = subset.Subsetter(options)
|
|
subsetter.populate(unicodes=[int(code, 16) for code in tile_icons.GLYPHS.values()])
|
|
subsetter.subset(font)
|
|
# The source stores xMin 0 with left bearing 0; saving recalculates xMin, and browsers
|
|
# then shift each glyph left by the difference. Keep the bearing equal to the real xMin.
|
|
glyf, hmtx = font['glyf'], font['hmtx']
|
|
for name in font.getGlyphOrder():
|
|
glyph = glyf[name]
|
|
glyph.recalcBounds(glyf)
|
|
hmtx[name] = (hmtx[name][0], getattr(glyph, 'xMin', 0))
|
|
buffer = io.BytesIO()
|
|
font.flavor = 'woff'
|
|
font.save(buffer)
|
|
return buffer.getvalue()
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--check', action='store_true')
|
|
args = parser.parse_args()
|
|
verify()
|
|
for path in PROFILES:
|
|
expected = profile(path.read_text())
|
|
if args.check:
|
|
if path.read_text() != expected: raise SystemExit(f'Outdated icon fonts: {path.name}')
|
|
else:
|
|
path.write_text(expected)
|
|
if args.check:
|
|
# Compare glyph coverage, not bytes: font output differs between fontTools versions.
|
|
wanted = {int(code, 16) for code in tile_icons.GLYPHS.values()}
|
|
if not WEB_FONT.exists() or set(TTFont(WEB_FONT).getBestCmap()) != wanted:
|
|
raise SystemExit(f'Outdated: {WEB_FONT.name}')
|
|
else:
|
|
WEB_FONT.write_bytes(web_font())
|
|
print(f'{len(tile_icons.ICONS)} pickable icons, {len(tile_icons.GLYPHS)} glyphs: ' + ('verified' if args.check else 'generated'))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|