v0.1.1 — structural renderer with safe area and 40x24 grid baseline

This commit is contained in:
Stu Leak 2025-11-03 10:29:05 -05:00
parent 4dab3c8015
commit fa33334823
9 changed files with 132 additions and 48 deletions

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Modeseven.ttf Executable file

Binary file not shown.

16
config/config.json Normal file
View File

@ -0,0 +1,16 @@
{
"Mode": "Broadcaster",
"ScreenWidth": 800,
"ScreenHeight": 600,
"Font": {
"Name": "Modeseven",
"Path": "assets/fonts/Modeseven.ttf",
"Size": 18
},
"ShowGrid": false,
"Colours": {
"Header": "blue",
"Footer": "red",
"Background": "black"
}
}

23
main.py
View File

@ -1,22 +1,23 @@
"""
Telefact minimal runner
- Opens a 4:3 800×600 window
- Renders header (blue band) and footer (red band)
- No formatter yet; thats next.
"""
import tkinter as tk import tkinter as tk
from src.config_manager import ConfigManager
from src.telefact_renderer import TelefactRenderer from src.telefact_renderer import TelefactRenderer
from src.core.telefact_frame import TelefactFrame from src.core.telefact_frame import TelefactFrame
def main(): def main():
config = ConfigManager().config
root = tk.Tk() root = tk.Tk()
root.title("Telefact — Broadcaster Prototype") root.title(f"Telefact — Broadcaster ({config['Mode']})")
renderer = TelefactRenderer(
root,
width=config["ScreenWidth"],
height=config["ScreenHeight"],
show_grid=config.get("ShowGrid", False),
font_path=config["Font"]["Path"]
)
renderer = TelefactRenderer(root, width=800, height=600, show_grid=False)
frame = TelefactFrame() frame = TelefactFrame()
# put a couple test glyphs so you can confirm alignment quickly
for i, ch in enumerate("TELEFACT"): for i, ch in enumerate("TELEFACT"):
frame.set_cell(2 + i, 2, ch, "yellow") frame.set_cell(2 + i, 2, ch, "yellow")
for i, ch in enumerate("BROADCASTER BASE"): for i, ch in enumerate("BROADCASTER BASE"):

Binary file not shown.

22
src/config_manager.py Normal file
View File

@ -0,0 +1,22 @@
import json
import os
class ConfigManager:
def __init__(self, config_path: str = "config/config.json"):
self.config_path = config_path
self.config = self._load()
def _load(self):
if not os.path.exists(self.config_path):
raise FileNotFoundError(f"Config file not found: {self.config_path}")
with open(self.config_path, "r", encoding="utf-8") as f:
return json.load(f)
def get(self, key, default=None):
"""Return a top-level config key, e.g. 'Font' or 'Mode'."""
return self.config.get(key, default)
def save(self):
"""Write any changes back to disk."""
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2)

View File

@ -1,10 +1,13 @@
""" """
Telefact Renderer (Tkinter) Telefact Renderer (Tkinter)
- Renders a TelefactFrame to a Canvas. - Renders a 40×24 Telefact grid within a safe-area margin.
- No header/footer bands; for layout calibration only.
- Pure Python (stdlib only). - Pure Python (stdlib only).
""" """
import os
import tkinter as tk import tkinter as tk
from tkinter import font as tkfont
from typing import Dict from typing import Dict
from src.core.telefact_frame import TelefactFrame from src.core.telefact_frame import TelefactFrame
@ -12,81 +15,123 @@ PALETTE: Dict[str, str] = {
"black": "#000000", "black": "#000000",
"blue": "#0000FF", "blue": "#0000FF",
"red": "#FF0000", "red": "#FF0000",
"magenta":"#FF00FF", "magenta": "#FF00FF",
"green": "#00FF00", "green": "#00FF00",
"cyan": "#00FFFF", "cyan": "#00FFFF",
"yellow": "#FFFF00", "yellow": "#FFFF00",
"white": "#FFFFFF", "white": "#FFFFFF",
} }
class TelefactRenderer: class TelefactRenderer:
def __init__(self, root: tk.Tk, width: int = 800, height: int = 600, show_grid: bool = False): def __init__(
self,
root: tk.Tk,
width: int = 880,
height: int = 660,
show_grid: bool = True,
colors: dict | None = None,
font_path: str | None = None,
font_size: int = 18,
):
self.root = root self.root = root
self.width = width self.width = width
self.height = height self.height = height
self.show_grid = show_grid self.show_grid = show_grid
# 40×24 grid for Telefact pages # Safe area for 40×24 grid (simulate CRT overscan)
self.cols, self.rows = 40, 24 self.cols, self.rows = 40, 24
self.cell_w = max(1, width // self.cols) self.margin_x = 24
self.cell_h = max(1, height // self.rows) self.margin_y = 24
self.w = self.cell_w * self.cols self.inner_w = width - self.margin_x * 2
self.h = self.cell_h * self.rows self.inner_h = height - self.margin_y * 2
self.cell_w = self.inner_w // self.cols
self.cell_h = self.inner_h // self.rows
self.w = self.inner_w
self.h = self.inner_h
# Colours
self.colors = {
"Background": "black",
"Grid": "#303030"
}
if colors:
self.colors.update(colors)
# Canvas covers the whole window
self.canvas = tk.Canvas( self.canvas = tk.Canvas(
root, width=self.w, height=self.h, root,
highlightthickness=0, bg=PALETTE["black"] width=self.width,
height=self.height,
highlightthickness=0,
bg=PALETTE.get(self.colors["Background"], self.colors["Background"]),
) )
self.canvas.pack() self.canvas.pack()
# simple monospace font; will swap for Teletext font later # Font setup
self.font = ("Courier New", max(12, self.cell_h - 9), "bold") self.font = self._load_font(font_path, font_size)
# ----- low-level drawing helpers ----- # ----------------------------------------------------------------------
def _load_font(self, font_path: str | None, font_size: int) -> tuple:
if font_path and os.path.exists(font_path):
try:
font_name = os.path.splitext(os.path.basename(font_path))[0]
try:
self.root.tk.call(
"font", "create", font_name, "-family", font_name, "-size", font_size
)
except tk.TclError:
pass
print(f"[info] Loaded Telefact font: {font_name}")
return (font_name, font_size)
except Exception as e:
print(f"[warn] Could not load custom font ({e}). Falling back to Courier New.")
elif font_path:
print(f"[warn] Font path not found: {font_path}. Using Courier New.")
return ("Courier New", font_size, "bold")
# ----------------------------------------------------------------------
def _gx(self, col: int) -> int: def _gx(self, col: int) -> int:
return col * self.cell_w return self.margin_x + col * self.cell_w
def _gy(self, row: int) -> int: def _gy(self, row: int) -> int:
return row * self.cell_h return self.margin_y + row * self.cell_h
def _fill_row(self, row: int, color: str) -> None:
self.canvas.create_rectangle(
self._gx(0), self._gy(row), self._gx(self.cols), self._gy(row + 1),
fill=PALETTE.get(color, color), width=0
)
def _draw_char(self, col: int, row: int, char: str, color: str) -> None: def _draw_char(self, col: int, row: int, char: str, color: str) -> None:
"""Draw a single glyph."""
x = self._gx(col) + 2 x = self._gx(col) + 2
y = self._gy(row) + self.cell_h // 2 y = self._gy(row) + self.cell_h // 2
self.canvas.create_text( self.canvas.create_text(
x, y, anchor="w", text=char, x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color)
font=self.font, fill=PALETTE.get(color, color)
) )
def _draw_grid(self) -> None: def _draw_grid(self) -> None:
"""Debug overlay for exact cell spacing."""
for c in range(self.cols + 1): for c in range(self.cols + 1):
x = self._gx(c) x = self._gx(c)
self.canvas.create_line(x, 0, x, self.h, fill="#202020") self.canvas.create_line(
x, self.margin_y, x, self.margin_y + self.h, fill=self.colors["Grid"]
)
for r in range(self.rows + 1): for r in range(self.rows + 1):
y = self._gy(r) y = self._gy(r)
self.canvas.create_line(0, y, self.w, y, fill="#202020") self.canvas.create_line(
self.margin_x, y, self.margin_x + self.w, y, fill=self.colors["Grid"]
)
# ----- public API ----- # ----------------------------------------------------------------------
def render(self, frame: TelefactFrame) -> None: def render(self, frame: TelefactFrame) -> None:
"""Paint the whole frame to the canvas.""" """Render just the text grid; no header/footer bars."""
self.canvas.delete("all") self.canvas.delete("all")
# background # Fill total background
self.canvas.create_rectangle(0, 0, self.w, self.h, self.canvas.create_rectangle(
fill=PALETTE["black"], width=0) 0, 0, self.width, self.height,
fill=PALETTE.get(self.colors["Background"], self.colors["Background"]),
width=0,
)
# header/footer colour bands # Draw Teletext cells
for r in frame.header_region():
self._fill_row(r, "blue")
for r in frame.footer_region():
self._fill_row(r, "red")
# draw text cells
for row in range(frame.rows): for row in range(frame.rows):
for col in range(frame.cols): for col in range(frame.cols):
ch, fg = frame.grid[row][col] ch, fg = frame.grid[row][col]