Zeilenhöhe-Regler statt "+ Text"; Linienstil editierbar; DOSSIER-Audit gesichert

- "+ Text"-Knopf entfernt (war ohnehin immer deaktiviert, kein Werkzeug
  dahinter — Text wird ein eigenständiges Zeichenwerkzeug, AUDIT B1).
  An seiner Stelle ein Zeilenhöhe-Regler (Stepper, Vielfaches der
  Schriftgrösse) — neues Paragraph.lineHeight, durchgereicht bis in
  HTML-Vorschau (line-height) und SVG-Render (kumulierte Zeilen-
  Vorschübe statt festem lineGap).
- Linienstil im Attribute-Panel war rein informativ (kein Setter im
  Host-Kontrakt). onSetSelectionLineStyle ergänzt (nur Drawing2D),
  jetzt echtes Dropdown statt "—"-Anzeige.
- docs/design/dossier-feature-audit.md: der ausführliche A1-A6/B1-B4/
  C1-C3/D1-D3/E-Auditbericht aus einer alten Session gesichert (lag
  bisher nur im Transkript, nicht im Repo) — Quelle der HANDOVER.md-
  AUDIT-Kürzel.
This commit is contained in:
2026-07-04 05:36:18 +02:00
parent 8b6b9291a1
commit d0b94a75ec
11 changed files with 307 additions and 65 deletions
+18 -6
View File
@@ -85,10 +85,13 @@ export function docToHtml(doc: RichTextDoc, opts: HtmlOptions = {}): string {
}
function paragraphToHtml(p: Paragraph, basePt: number, color?: string): string {
const align = p.align && p.align !== "left" ? ` style="text-align:${p.align}"` : "";
if (p.runs.length === 0) return `<div${align}><br></div>`;
const styles: string[] = [];
if (p.align && p.align !== "left") styles.push(`text-align:${p.align}`);
if (p.lineHeight != null) styles.push(`line-height:${p.lineHeight}`);
const style = styles.length ? ` style="${styles.join(";")}"` : "";
if (p.runs.length === 0) return `<div${style}><br></div>`;
const inner = p.runs.map((r) => runToHtmlSpan(r, basePt, color)).join("");
return `<div${align}>${inner}</div>`;
return `<div${style}>${inner}</div>`;
}
function runToHtmlSpan(run: TextRun, basePt: number, color?: string): string {
@@ -119,6 +122,8 @@ export interface SvgTspan {
export interface SvgLine {
tspans: SvgTspan[];
align: "left" | "center" | "right";
/** Zeilenhöhe dieses Absatzes (Vielfaches der Basisgrösse), s. `Paragraph.lineHeight`. */
lineHeight: number;
}
export interface SvgTextOptions {
@@ -126,7 +131,12 @@ export interface SvgTextOptions {
x: number;
/** Baseline der ERSTEN Zeile, Y in Benutzereinheiten. */
y: number;
/** Zeilenvorschub (Baseline zu Baseline) in Benutzereinheiten. */
/**
* Zeilenvorschub (Baseline zu Baseline) in Benutzereinheiten für Zeilen OHNE
* eigene `lineHeight` (Rückwärts-Kompatibilität/Fallback). Absätze mit
* gesetzter `Paragraph.lineHeight` berechnen ihren Vorschub stattdessen
* selbst aus `basePt · unitPerPt · lineHeight`.
*/
lineHeight: number;
/**
* Umrechnung Schriftgrösse: `sizePt` × `unitPerPt` = Grösse in Benutzereinheiten.
@@ -142,13 +152,15 @@ export interface SvgTextOptions {
/**
* Zerlegt das Dokument in Zeilen/Tspans mit bereits in Benutzereinheiten
* umgerechneten Grössen. Der Aufrufer positioniert die Zeilen selbst (Baseline
* der Zeile i = `y + lineHeight`).
* der Zeile i = `y + Σ lineHeight[0..i-1]`, s. `docToSvgText`).
*/
export function docToLines(doc: RichTextDoc, opts: SvgTextOptions): SvgLine[] {
const basePt = opts.basePt ?? 12;
const color = opts.color ?? "#000000";
const baseUnit = basePt * opts.unitPerPt;
return doc.paragraphs.map((p) => ({
align: p.align ?? "left",
lineHeight: p.lineHeight != null ? baseUnit * p.lineHeight : opts.lineHeight,
tspans: p.runs
.filter((r) => r.text.length > 0)
.map((r) => runToTspan(r, basePt, opts.unitPerPt, color)),
@@ -184,7 +196,7 @@ export function docToSvgText(doc: RichTextDoc, opts: SvgTextOptions): string {
const lines = docToLines(doc, opts);
const parts: string[] = [];
lines.forEach((line, i) => {
const dy = i === 0 ? 0 : opts.lineHeight;
const dy = i === 0 ? 0 : line.lineHeight;
const anchor = line.align === "center" ? "middle" : line.align === "right" ? "end" : "start";
const runSpans = line.tspans.map((t) => tspanToSvg(t)).join("");
// Leere Zeile: ein Zero-Width-Space, damit dy dennoch wirkt.
+6 -1
View File
@@ -44,10 +44,15 @@ export interface TextRun {
/** Absatz-Ausrichtung. */
export type Align = "left" | "center" | "right";
/** Ein Absatz: eine Zeile aus Runs plus optionale Ausrichtung. */
/** Zeilenhöhe als Vielfaches der Basisgrösse, wenn nicht am Absatz gesetzt. */
export const DEFAULT_LINE_HEIGHT = 1.3;
/** Ein Absatz: eine Zeile aus Runs plus optionale Ausrichtung/Zeilenhöhe. */
export interface Paragraph {
runs: TextRun[];
align?: Align;
/** Zeilenhöhe als Vielfaches der Basisgrösse (z. B. 1.3 = 130 %). */
lineHeight?: number;
}
/** Das gesamte Rich-Text-Dokument. */