"""Boards of each package, from a checked layout.""" from __future__ import annotations from dataclasses import dataclass from .data import Data from .layout import BACK, CoreLayout, MotorLayout from .obf import Board, Button, uid @dataclass class Package: file: str # path under data/pagesets set: str size: str variant: str # "full", "reveal-3x3", "filter-starter" boards: list[Board] root: str manifest: dict def _evidence(data: Data, word: str) -> dict: out: dict = {} lists = data.in_aac_lists(word) if lists: out["lists"] = lists rank = data.spoken_rank.get(word.lower()) if rank: out["spoken_rank"] = rank app = data.app_vocabulary.get(word.lower()) if app: out["app_level"] = app[2] return out def _word_button(data: Data, key: str, cell, hidden: bool, ext: dict) -> Button: e = data.entry(key) ps = {"kind": "word", "pos": e.pos, **ext, **_evidence(data, key)} if e.sense: ps["sense"] = e.sense return Button(key, cell[0], cell[1], "speak", e.role, e.word_class, hidden=hidden, pageset=ps, sense=e.sense if e.unglossed else None) # ════════════════════════════════════════════════════════════════════════ # Growing Core # ════════════════════════════════════════════════════════════════════════ def check_key_symbols(data: Data, name: str, ks: dict[str, str], keys: set[str]) -> None: """A key's chosen picture is a concept of the symbol index, and no two keys share one (DESIGN-motor.md, "One meaning per location").""" from .obf import lookup_concept problems = [f"{k}: {c!r} is not a concept of the symbol index" for k, c in ks.items() if not lookup_concept(data, c)] problems += [f"{k}: not a page or group key" for k in ks if k not in keys] seen: dict[str, str] = {} for k, c in ks.items(): if c in seen: problems.append(f"{k} and {seen[c]} share the picture {c!r}") seen[c] = k if problems: raise SystemExit(f"{name} key_symbols:\n " + "\n ".join(problems)) def check_labels(pkg: "Package") -> None: """On a board, no page key has the same words as a word cell: two buttons that read alike never mean two things on one page. A Back control is drawn as its arrow, with no words (the app's grid_cell.rs), so the word "back" may sit beside it.""" problems = [] for b in pkg.boards: words = {btn.key.lower() for btn in b.buttons if btn.action == "speak"} for btn in b.buttons: if btn.action == "navigate" and btn.control != "back" and btn.key.lower() in words: problems.append(f"{b.name}: the key {btn.key!r} reads like the word on the same board") if problems: raise SystemExit(f"{pkg.file}:\n " + "\n ".join(problems)) def core_packages(data: Data, lay: CoreLayout) -> list[Package]: d = data.growing_core check_key_symbols(data, "growing-core", d.get("key_symbols", {}), set(d["pages"])) out = [] for k, size in enumerate(lay.sizes): out.append(_core_package(data, lay, k, None)) for name in d.get("reveal", []): size_name, stage_name = name.split(":") k = [s.name for s in lay.sizes].index(size_name) r = [s.name for s in lay.sizes].index(stage_name) out.append(_core_package(data, lay, k, r)) return out def _core_package(data: Data, lay: CoreLayout, k: int, reveal: int | None) -> Package: d = data.growing_core size = lay.sizes[k] names = [s.name for s in lay.sizes] variant = "full" if reveal is None else f"reveal-{names[reveal]}" tag = f"{size.name}" if reveal is None else f"{size.name}-{variant}" core_id = uid("growing-core", tag, "core") opener_stage = {p.key: p.stage for p in lay.core if p.kind == "page"} page_ids = {name: uid("growing-core", tag, "page", name) for name in lay.pages if opener_stage[name] <= k} key_symbols = d.get("key_symbols", {}) buttons = [] for p in lay.core: if p.stage > k: continue cell = p.at[size.name] hidden = reveal is not None and p.stage > reveal # shown_from: the step of the set's growth it is first shown at (aac-core # samewords: the key shown first keeps a word two keys share). ext = {"stage": names[p.stage], "shown_from": p.stage, "point": list(p.point)} if p.kind == "page": page = lay.pages[p.key] buttons.append( Button(page.label, cell[0], cell[1], "navigate", page.role, target=page_ids[p.key], symbol=key_symbols.get(p.key), hidden=hidden, pageset={"kind": "page", **ext}) ) else: buttons.append(_word_button(data, p.key, cell, hidden, ext)) boards = [Board(core_id, f"Growing Core {size.name}", size.rows, size.cols, buttons, {"set": "growing-core", "board": "core", "size": size.name, "variant": variant})] for name, pid in page_ids.items(): page = lay.pages[name] start = opener_stage[name] pb = [] for p in page.items: g = start + p.stage # its stage on the core's ladder if g > k: continue cell = p.at[size.name] hidden = reveal is not None and g > reveal and p.kind != "back" ext = {"stage": names[g], "shown_from": g, "point": list(p.point)} if p.kind == "back": pb.append(Button(BACK, cell[0], cell[1], "navigate", "control", target=core_id, control="back", pageset={"kind": "back"})) elif p.kind == "modifier": pb.append(Button(p.key, cell[0], cell[1], "modifier", "little", modifier=page.modifiers[p.key], hidden=hidden, pageset={"kind": "modifier", **ext})) else: pb.append(_word_button(data, p.key, cell, hidden, ext)) boards.append(Board(pid, page.label, size.rows, size.cols, pb, {"set": "growing-core", "board": "page", "page": name, "size": size.name, "variant": variant})) manifest = { "set": "growing-core", "title": d["title"], "version": d["version"], "size": size.name, "ladder": names, "variant": variant, "reveal_stage": names[reveal] if reveal is not None else None, "licence": "CC-BY-SA-4.0", "sources": sorted(data.catalog["sources"]), } file = f"growing-core/growing-core-{tag}.obz" pkg = Package(file, "growing-core", size.name, variant, boards, core_id, manifest) check_labels(pkg) return pkg # ════════════════════════════════════════════════════════════════════════ # Fixed Paths # ════════════════════════════════════════════════════════════════════════ def motor_filter(data: Data, name: str) -> set[str]: spec = data.fixed_paths["filters"][name] words: set[str] = set() for src in spec["from"]: lst = data.universal_core if src == "universal_core" else data.banajee for w in lst: words.add("all done" if w.lower() in ("finished", "all done") else w) lex = {k.lower(): k for k in data.lexicon} return {lex.get(w.lower(), w) for w in words} def motor_packages(data: Data, lay: MotorLayout) -> list[Package]: d = data.fixed_paths keys = set(d["pages"]) | {g for spec in d["pages"].values() for g in spec.get("groups", {})} check_key_symbols(data, "fixed-paths", d.get("key_symbols", {}), keys) out = [] for ms in lay.sizes: out.append(_motor_package(data, lay, ms, None)) for f in d.get("filters", {}): out.append(_motor_package(data, lay, ms, f)) return out def _motor_package(data: Data, lay: MotorLayout, ms, filt: str | None) -> Package: d = data.fixed_paths variant = "full" if filt is None else f"filter-{filt}" tag = ms.label if filt is None else f"{ms.label}-{filt}" show = None if filt is None else motor_filter(data, filt) # The set grows from its first words (the starter filter) to the rest: # shown_from 0 and 1, in every variant, so that a word two keys share # stays where it was shown first (aac-core samewords). first = motor_filter(data, "starter") if "starter" in d.get("filters", {}) else None def step(key: str) -> dict: return {} if first is None else {"shown_from": 0 if key in first else 1} home_id = uid("fixed-paths", tag, "home") page_ids = {p: uid("fixed-paths", tag, "page", p) for p in ms.pages} group_ids = {g: uid("fixed-paths", tag, "group", g) for g in ms.groups} ks = d.get("key_symbols", {}) size = ms.size def behind_page(p): spec = ms.pages[p] return list(spec["words"]) + [w for g in spec["groups"] for w in ms.groups[g]["words"]] def visible(words) -> bool: return show is None or any(w in show for w in words) mods = {m["label"]: m["modifier"] for m in d["pages"]["Word forms"].get("modifiers", [])} home_buttons = [] for key, cell in ms.home.items(): if key in ms.pages: home_buttons.append(Button(key, cell[0], cell[1], "navigate", d["pages"][key]["role"], target=page_ids[key], symbol=ks.get(key), hidden=not visible(behind_page(key)), pageset={"kind": "page", "path": [key]})) else: home_buttons.append(_word_button(data, key, cell, show is not None and key not in show, {"path": [key], "hits": 1, **step(key)})) boards = [Board(home_id, f"Fixed Paths {ms.label}", size.rows, size.cols, home_buttons, {"set": "fixed-paths", "board": "home", "size": ms.label, "grid": size.name, "variant": variant})] for page, spec in ms.pages.items(): pb = [Button(BACK, spec["close"][0], spec["close"][1], "navigate", "control", target=home_id, control="back", pageset={"kind": "close"})] for w, cell in spec["words"].items(): if w in mods: pb.append(Button(w, cell[0], cell[1], "modifier", "little", modifier=mods[w], hidden=show is not None, pageset={"kind": "modifier", "path": [page, w], "hits": 2, **({} if first is None else {"shown_from": 1})})) else: pb.append(_word_button(data, w, cell, show is not None and w not in show, {"path": [page, w], "hits": 2, **step(w)})) for g, cell in spec["groups"].items(): pb.append(Button(g, cell[0], cell[1], "navigate", d["pages"][page]["role"], target=group_ids[g], symbol=ks.get(g), hidden=not visible(ms.groups[g]["words"]), pageset={"kind": "group", "path": [page, g]})) boards.append(Board(page_ids[page], page, size.rows, size.cols, pb, {"set": "fixed-paths", "board": "page", "page": page, "size": ms.label, "variant": variant})) for g, spec in ms.groups.items(): gb = [Button(BACK, spec["close"][0], spec["close"][1], "navigate", "control", target=page_ids[spec["page"]], control="back", pageset={"kind": "close"})] for w, cell in spec["words"].items(): gb.append(_word_button(data, w, cell, show is not None and w not in show, {"path": [spec["page"], g, w], "hits": 3, **step(w)})) boards.append(Board(group_ids[g], g, size.rows, size.cols, gb, {"set": "fixed-paths", "board": "group", "page": spec["page"], "group": g, "size": ms.label, "variant": variant})) manifest = { "set": "fixed-paths", "title": d["title"], "version": d["version"], "size": ms.label, "grid": size.name, "variant": variant, "filter": filt, "filter_words": sorted(show) if show is not None else None, "recommends": { "return_home_after_word": True, "why": "Every path starts at Home. SPEC §5.6 item 6 leaves this to the person: ask, never turn it on.", }, "licence": "CC-BY-SA-4.0", "sources": sorted(data.catalog["sources"]), } file = f"fixed-paths/fixed-paths-{tag}.obz" pkg = Package(file, "fixed-paths", ms.label, variant, boards, home_id, manifest) check_labels(pkg) return pkg