Custom icon per tile: 150 MDI icons in firmware and editor
- 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>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user