v0.1.1 — structural renderer with safe area and 40x24 grid baseline
This commit is contained in:
parent
4dab3c8015
commit
fa33334823
BIN
assets/fonts/EuropeanTeletext.ttf
Normal file
BIN
assets/fonts/EuropeanTeletext.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/EuropeanTeletextNuevo.ttf
Normal file
BIN
assets/fonts/EuropeanTeletextNuevo.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Modeseven.ttf
Executable file
BIN
assets/fonts/Modeseven.ttf
Executable file
Binary file not shown.
16
config/config.json
Normal file
16
config/config.json
Normal 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
23
main.py
|
|
@ -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; that’s next.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from src.config_manager import ConfigManager
|
||||
from src.telefact_renderer import TelefactRenderer
|
||||
from src.core.telefact_frame import TelefactFrame
|
||||
|
||||
def main():
|
||||
config = ConfigManager().config
|
||||
|
||||
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()
|
||||
|
||||
# put a couple test glyphs so you can confirm alignment quickly
|
||||
for i, ch in enumerate("TELEFACT"):
|
||||
frame.set_cell(2 + i, 2, ch, "yellow")
|
||||
for i, ch in enumerate("BROADCASTER BASE"):
|
||||
|
|
|
|||
BIN
src/__pycache__/config_manager.cpython-313.pyc
Normal file
BIN
src/__pycache__/config_manager.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
22
src/config_manager.py
Normal file
22
src/config_manager.py
Normal 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)
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
"""
|
||||
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).
|
||||
"""
|
||||
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont
|
||||
from typing import Dict
|
||||
from src.core.telefact_frame import TelefactFrame
|
||||
|
||||
|
|
@ -19,74 +22,116 @@ PALETTE: Dict[str, str] = {
|
|||
"white": "#FFFFFF",
|
||||
}
|
||||
|
||||
|
||||
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.width = width
|
||||
self.height = height
|
||||
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.cell_w = max(1, width // self.cols)
|
||||
self.cell_h = max(1, height // self.rows)
|
||||
self.w = self.cell_w * self.cols
|
||||
self.h = self.cell_h * self.rows
|
||||
self.margin_x = 24
|
||||
self.margin_y = 24
|
||||
self.inner_w = width - self.margin_x * 2
|
||||
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(
|
||||
root, width=self.w, height=self.h,
|
||||
highlightthickness=0, bg=PALETTE["black"]
|
||||
root,
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
highlightthickness=0,
|
||||
bg=PALETTE.get(self.colors["Background"], self.colors["Background"]),
|
||||
)
|
||||
self.canvas.pack()
|
||||
|
||||
# simple monospace font; will swap for Teletext font later
|
||||
self.font = ("Courier New", max(12, self.cell_h - 9), "bold")
|
||||
# Font setup
|
||||
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:
|
||||
return col * self.cell_w
|
||||
return self.margin_x + col * self.cell_w
|
||||
|
||||
def _gy(self, row: int) -> int:
|
||||
return 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
|
||||
)
|
||||
return self.margin_y + row * self.cell_h
|
||||
|
||||
def _draw_char(self, col: int, row: int, char: str, color: str) -> None:
|
||||
"""Draw a single glyph."""
|
||||
x = self._gx(col) + 2
|
||||
y = self._gy(row) + self.cell_h // 2
|
||||
self.canvas.create_text(
|
||||
x, y, anchor="w", text=char,
|
||||
font=self.font, fill=PALETTE.get(color, color)
|
||||
x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color)
|
||||
)
|
||||
|
||||
def _draw_grid(self) -> None:
|
||||
"""Debug overlay for exact cell spacing."""
|
||||
for c in range(self.cols + 1):
|
||||
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):
|
||||
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:
|
||||
"""Paint the whole frame to the canvas."""
|
||||
"""Render just the text grid; no header/footer bars."""
|
||||
self.canvas.delete("all")
|
||||
|
||||
# background
|
||||
self.canvas.create_rectangle(0, 0, self.w, self.h,
|
||||
fill=PALETTE["black"], width=0)
|
||||
# Fill total background
|
||||
self.canvas.create_rectangle(
|
||||
0, 0, self.width, self.height,
|
||||
fill=PALETTE.get(self.colors["Background"], self.colors["Background"]),
|
||||
width=0,
|
||||
)
|
||||
|
||||
# header/footer colour bands
|
||||
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
|
||||
# Draw Teletext cells
|
||||
for row in range(frame.rows):
|
||||
for col in range(frame.cols):
|
||||
ch, fg = frame.grid[row][col]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user