Work in progress after the coordinator's decisions of 2026-10-05; does not compile yet (aac-web chooser). Done so far: - aac-core: WordCategory::Conjunction with Fitzgerald colours for every class; nav module (Back/Home words moved from aac-web, shared with OBF export: Back/Home cells never carry the gloss); Cell.sense (a key used in another sense than its glosses'); pagesets module (catalogue, stages, reveal). - tools/pagesets: Fixed Paths 30 beginner Home; "back" the word in both sets; conjunction class; catalogue (catalog.py, generators.json) with the committed data/pagesets/index.json; packages no longer committed. - aac-web: words with no word in the person's language hidden in place, Edit mode says so; Back drawn as its arrow; catalogue chooser started. Co-Authored-By: Claude Opus 5.5 <[email protected]>
262 lines
12 KiB
Python
262 lines
12 KiB
Python
"""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
|
|
ext = {"stage": names[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], "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)
|
|
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}))
|
|
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}))
|
|
else:
|
|
pb.append(_word_button(data, w, cell, show is not None and w not in show, {"path": [page, w], "hits": 2}))
|
|
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}))
|
|
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
|