Skip to content

Übungsblatt mit Text: Vorschrift lines on the ruling with a grey trace row, composed like the Federprobe and printed in the browser - #458

Open
MarkusNeusinger wants to merge 3 commits into
mainfrom
claude/uebungsblatt-text
Open

Übungsblatt mit Text: Vorschrift lines on the ruling with a grey trace row, composed like the Federprobe and printed in the browser#458
MarkusNeusinger wants to merge 3 commits into
mainfrom
claude/uebungsblatt-text

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

What

The Übungsblatt takes a text. A „Vorschrift (optional)" field on /schreiben/uebungsblatt: every line is set in the written script (Sütterlin) on a row of the chosen ruling, a grey copy follows on the next row to trace over („Nachspur-Zeile in Grau", switchable, on by default), then empty rows to write it freely („Leerzeilen je Vorschrift", default 2).

  • Composition is the Federprobe's: GET /write/word per line through the shared render cache, debounced (450 ms), answers remembered by their text so a composition is never shown under a line that has changed (sections/worksheet/useWorksheetText.ts).
  • Placement is pure and tested (lib/uebungstext.ts, placeText): template units onto the row baseline with the Mittelband as x-height; buildLineature now reports its rows. The SVG preview and the PDF draw the same InkShape list — ContentStream.polyline for the connectors (new), even-odd fills for the letters.
  • Honest limits, named under the field instead of drawn: a pending line keeps its row (no jumping while typing), a line too wide for this Mittellänge or without a row left is listed, letters not yet written are listed (lettersFromKeys moved to domain/glyphs; the Federprobe and the Lesart page share it now). The download waits while a line is pending.
  • Copy (intro, landing, hub, SEO) mentions the Vorschrift; prerender regenerated; glossary entry „Vorschrift"; frontend-stack rows; CHANGELOG.

Why

Website audit 2026-08-29, item 7/8 — vision §2's content-aware practice sheet („Text + passende Lineatur in einem Schritt").

Deviation from the documented plan — your call at merge

architektur.md §15 (and the audit task) planned POST /worksheet + WeasyPrint in the API. This PR ships the sheet client-side instead: the text arrives composed from /write/word — the path the Federprobe already uses — and lib/pdf.ts, the Lesetafel's builder since #454, already fills rings and strokes polylines, so the missing piece was placement only (~100 pure lines). WeasyPrint would have added a 200–400 MB rendering dependency to the API image (just halved to 512 Mi in #455) plus a new route for one page. §15 carries a dated note; the WeasyPrint path stays documented as the option for multi-page, HTML-set sheets. If you would rather have the backend path, say so and I will redo it there.

Limits (by design, shown in the UI)

One page. A line must fit the writing width at the chosen Mittellänge (about 20 characters at the Sütterlin preset's 6 mm, about 45 at Kurrent's 2.5 mm) — a longer line is named as too wide, not shrunk, so the model always matches the ruling. The text is always set in the written Vorlage (Sütterlin), whatever ruling is chosen; the (i) says so.

Verification

/verify-frontend on the dev server against the deployed API: three lines typed → two placed with their grey trace rows, the third named as too wide for 6 mm; preview and the downloaded PDF match (rendered with PyMuPDF); desktop and mobile viewports; console clean. tsc, eslint (no new warnings), vitest green (uebungstext.test.ts: line splitting, placement, trace row, overflow, PDF ops); prerender drift test passes.

Copilot AI lite review requested due to automatic review settings August 29, 2026 23:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

placeText fails to reserve rows for composed lines with zero draw items, which can cause later lines to shift unexpectedly and misrepresent “no jumping” behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a content-aware “Übungstext” to the /schreiben/uebungsblatt worksheet: each input line is composed via the existing /write/word render path (Federprobe-style), then placed onto the selected ruling rows in the browser (with optional grey trace row and configurable empty practice rows), and rendered consistently in both SVG preview and the downloaded PDF.

Changes:

  • Introduces client-side composition+placement for worksheet text (useWorksheetText.ts, lib/uebungstext.ts) and wires it into the worksheet UI.
  • Extends the shared worksheet renderers to draw composed ink shapes in both preview SVG and PDF (PreviewSvg, lib/pdf.ts).
  • Updates docs/SEO/prerender and glossary/changelog to reflect the new “Vorschrift” feature.
File summaries
File Description
docs/reference/glossar.md Adds glossary entry + index mention for “Vorschrift”.
docs/reference/frontend-stack.md Documents the new worksheet text feature and module responsibilities.
docs/concepts/architektur.md Notes the client-side implementation status for the single-page text worksheet.
CHANGELOG.md Adds an [Unreleased] entry describing the feature and implementation approach.
app/src/sections/worksheet/WorksheetView.tsx Wires worksheet text state, placement, status, and PDF/preview rendering.
app/src/sections/worksheet/useWorksheetText.ts New hook to debounce and cache /write/word compositions per line text.
app/src/sections/worksheet/PreviewSvg.tsx Draws InkShape overlays (fills + connector polylines) in the SVG preview.
app/src/sections/worksheet/ConfigPanel.tsx Adds the “Vorschrift” textarea and options; disables download while pending.
app/src/sections/vergleichen/VergleichenView.tsx Uses shared lettersFromKeys helper from domain glyph registry.
app/src/sections/scribe/ScribeView.tsx Uses shared lettersFromKeys helper from domain glyph registry.
app/src/locales/de/worksheet.ts Adds German UI copy for the new worksheet text feature + status messages.
app/src/locales/de/seo.ts Updates worksheet SEO description to mention optional “Vorschrift” text.
app/src/locales/de/landing.ts Updates landing card copy to mention “Vorschrift” text.
app/src/locales/de/hub.ts Updates Schreiben hub copy to mention “Vorschrift” on the worksheet.
app/src/lib/uebungstext.ts New pure placement + limits reporting for composed lines into ruling rows.
app/src/lib/uebungstext.test.ts Adds unit tests for splitting/placement/trace row/overflow + PDF ops.
app/src/lib/seo/prerender.ts Includes the new worksheet text section in prerendered body content.
app/src/lib/pdf.ts Adds InkShape + ContentStream.polyline() and draws shapes in lineaturePdf.
app/src/lib/lineatur.ts Exposes per-row metrics (rows) alongside ruling segments/marks for placement.
app/src/domain/glyphs.ts Adds shared lettersFromKeys() helper for “missing glyphs” user notes.
app/prerender/schreiben/uebungsblatt.html Updates prerendered worksheet page content + meta descriptions.
app/prerender/schreiben.html Updates prerendered Schreiben hub copy to mention “Vorschrift”.
app/prerender/index.html Updates prerendered landing copy to mention “Vorschrift” on the worksheet.
Review details
  • Files reviewed: 23/23 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/src/lib/uebungstext.ts Outdated
@MarkusNeusinger

Copy link
Copy Markdown
Owner Author

Fixed in the last push: a composition with no writable items now reserves its rows exactly like a pending line (one branch for both), so nothing jumps when it resolves, and the missing-letter note under the field names what stays blank; test added.

MarkusNeusinger added a commit that referenced this pull request Aug 29, 2026
…g one

Review round on #458: a composition whose every letter is missing
(items empty) used to release its rows, so the lines below jumped up
once it resolved; it now reserves them like a pending line, and the
missing-letter note names what stays blank.
Copilot AI review requested due to automatic review settings August 29, 2026 23:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new useWorksheetText hook can return stale compositions across sourceId changes and can update state after unmount, and the worksheet PDF download revokes its object URL too aggressively (risking flaky downloads).

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

app/src/sections/worksheet/WorksheetView.tsx:145

  • The download URL is revoked with setTimeout(..., 0). That effectively revokes on the next tick, which can still race the download start on slower devices/browsers (and contradicts the comment). Other download helpers in the app defer revocation for seconds to avoid this.
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `lineatur-${presetId || 'eigen'}.pdf`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    // Defer revocation so it can't race the download start on slower devices.
    setTimeout(() => URL.revokeObjectURL(url), 0);
  };

app/src/sections/worksheet/useWorksheetText.ts:46

  • useWorksheetText caches requested/answers across sourceId changes and has no unmount guard for in-flight requests. If sourceId ever changes, the hook can return compositions from the previous sourceId without refetching; and if the component unmounts while requests are in flight, the promise handlers can still call setAnswers, causing React "setState on unmounted" warnings in dev.
export function useWorksheetText(text: string, sourceId: string = CONFIG.sourceId): WorksheetText {
  const [answers, setAnswers] = useState<Map<string, ComposedWordOut | 'error'>>(() => new Map());
  // Lines already asked for — a failure is removed again so the next edit retries it.
  const requested = useRef(new Set<string>());
  const wanted = useMemo(() => textLines(text), [text]);

  useEffect(() => {
    const todo = wanted.filter((line) => !requested.current.has(line));
    if (!todo.length) return;
    const timer = setTimeout(() => {
      for (const line of todo) {
        requested.current.add(line);
        fetchRenderWord(sourceId, line).then(
          (c) => setAnswers((m) => new Map(m).set(line, c)),
          () => {
            requested.current.delete(line);
            setAnswers((m) => new Map(m).set(line, 'error'));
          },
        );
      }
    }, DEBOUNCE_MS);
    return () => clearTimeout(timer);
  }, [wanted, sourceId]);
  • Files reviewed: 23/23 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…like the Federprobe, placed and printed in the browser

Each line of the worksheet's Übungstext is composed server-side
(GET /write/word through the shared render cache, debounced, kept by
its text) and placed by the pure lib/uebungstext.ts onto a row of the
chosen ruling — Mittelband as x-height, empty practice rows beneath, a
pending line keeping its row, lines too wide or without a row named
under the field instead of drawn, unwritten letters listed. The SVG
preview and the PDF draw the same InkShape list (ContentStream.polyline
for the connectors, even-odd fills for the letters); buildLineature now
reports its rows. Client-side on the Lesetafel's builder instead of the
WeasyPrint backend §15 planned — the doc carries the dated note.
lettersFromKeys moved to domain/glyphs (three pages share it).
Website audit 2026-08-29, item 7/8.
The audit item asks for grey letters to trace over (graue
Nachfahr-Buchstaben): placeText now sets a TRACE-coloured copy of each
model line on the row after it (switchable, on by default), the practice
rows follow; a copy without room at the page's end is dropped silently
while the model line stands. Test, locale, docs and changelog follow.
…g one

Review round on #458: a composition whose every letter is missing
(items empty) used to release its rows, so the lines below jumped up
once it resolved; it now reserves them like a pending line, and the
missing-letter note names what stays blank.
@MarkusNeusinger
MarkusNeusinger force-pushed the claude/uebungsblatt-text branch from f31fd17 to e2ef549 Compare August 30, 2026 05:43
Copilot AI review requested due to automatic review settings August 30, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

placeText currently fails to report “no row left” for pending/failed lines when the page has no remaining rows, which can suppress the intended UI status in offline/loading scenarios.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 23/23 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +86 to +92
if (!c || !c.items.length) {
// Pending, failed, or nothing writable (every letter still unwritten):
// the line keeps its rows so the sheet does not jump, and the
// missing-letter note names what stays blank.
if (rows[next]) next += step;
continue;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants