From d27622d581f98b111bbcd1f576045f3cc05cb32a Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 2 Jul 2026 19:05:01 +0200 Subject: [PATCH] Nativer 2D-Viewport: Raumstempel-Text (Einstrich-Vektorschrift) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues strokeFont.ts (kompakte Einstrich-Schrift: A-Z, 0-9, Symbole inkl. ²/·). toRenderScene wickelt Text-Primitive (Doc-Zeilen + Live-Zusatzzeilen) zu Glyph- Polylinien in Modell-Metern ab (vertikal um den Anker zentriert, Ausrichtung je Absatz) und speist sie wie die Schraffuren in die render2d-Szene. Erste Fassung in Versalien; render2d bleibt textrenderer-frei. Damit zeigt der native Grundriss den Raumstempel (Name/Fläche/SIA) analog zum Browser. --- src/plan/strokeFont.ts | 91 +++++++++++++++++++++++++++++++++++++++ src/plan/toRenderScene.ts | 49 ++++++++++++++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/plan/strokeFont.ts diff --git a/src/plan/strokeFont.ts b/src/plan/strokeFont.ts new file mode 100644 index 0000000..f559aa9 --- /dev/null +++ b/src/plan/strokeFont.ts @@ -0,0 +1,91 @@ +// Kompakte Einstrich-Vektorschrift für den nativen 2D-Viewport (render2d hat +// keinen Textrenderer). Jedes Glyph ist eine Liste von Strichen (Polylinien) in +// einem Raster: x in [0..5], y in [0..7], BASISLINIE y=0, Versalhöhe y=7, y nach +// OBEN (passt zu render2d, dessen to_screen y spiegelt). Kleinbuchstaben werden +// (erste Fassung) als Versalien gezeichnet. Ausgabe der Layout-Funktion sind +// Modell-Meter-Polylinien, die 1:1 in die render2d-Szene fließen. + +export type StrokePoint = [number, number]; +export type Glyph = { w: number; strokes: StrokePoint[][] }; + +// Rasterhöhe (Versalhöhe) in Font-Einheiten. +const CAP = 7; +// Standard-Vorschub (Glyphbreite inkl. Seitenabstand) in Font-Einheiten. +const ADV = 6; + +// Hilfen zum knappen Definieren. +const g = (w: number, ...strokes: StrokePoint[][]): Glyph => ({ w, strokes }); + +// Einstrich-Glyphen (bewusst geradlinig/einfach, gut lesbar). +const FONT: Record = { + " ": g(ADV), + A: g(ADV, [[0, 0], [2.5, 7], [5, 0]], [[1, 3], [4, 3]]), + B: g(ADV, [[0, 0], [0, 7], [3.5, 7], [4.5, 5.5], [3.5, 4], [0, 4]], [[3.5, 4], [4.7, 2], [3.5, 0], [0, 0]]), + C: g(ADV, [[5, 6], [3, 7], [1, 6], [0, 3.5], [1, 1], [3, 0], [5, 1]]), + D: g(ADV, [[0, 0], [0, 7], [3, 7], [5, 4.5], [5, 2.5], [3, 0], [0, 0]]), + E: g(ADV, [[5, 7], [0, 7], [0, 0], [5, 0]], [[0, 3.5], [3.5, 3.5]]), + F: g(ADV, [[5, 7], [0, 7], [0, 0]], [[0, 3.5], [3.5, 3.5]]), + G: g(ADV, [[5, 6], [3, 7], [1, 6], [0, 3.5], [1, 1], [3, 0], [5, 1], [5, 3], [3, 3]]), + H: g(ADV, [[0, 0], [0, 7]], [[5, 0], [5, 7]], [[0, 3.5], [5, 3.5]]), + I: g(3, [[1.5, 0], [1.5, 7]]), + J: g(ADV, [[4, 7], [4, 1.5], [3, 0], [1, 0], [0, 1.5]]), + K: g(ADV, [[0, 0], [0, 7]], [[5, 7], [0, 3.5], [5, 0]]), + L: g(ADV, [[0, 7], [0, 0], [5, 0]]), + M: g(7, [[0, 0], [0, 7], [3.5, 3], [7, 7], [7, 0]]), + N: g(ADV, [[0, 0], [0, 7], [5, 0], [5, 7]]), + O: g(ADV, [[1, 6], [0, 3.5], [1, 1], [3, 0], [4.5, 1.5], [5, 3.5], [4, 6], [3, 7], [1, 6]]), + P: g(ADV, [[0, 0], [0, 7], [3.5, 7], [5, 5.5], [3.5, 4], [0, 4]]), + Q: g(ADV, [[1, 6], [0, 3.5], [1, 1], [3, 0], [5, 2], [5, 4], [3, 7], [1, 6]], [[3, 2], [5, 0]]), + R: g(ADV, [[0, 0], [0, 7], [3.5, 7], [5, 5.5], [3.5, 4], [0, 4]], [[3, 4], [5, 0]]), + S: g(ADV, [[5, 6], [3, 7], [1, 6.5], [1, 4.5], [4, 3], [4.5, 1.5], [3, 0], [0.5, 0.8]]), + T: g(ADV, [[0, 7], [5, 7]], [[2.5, 7], [2.5, 0]]), + U: g(ADV, [[0, 7], [0, 1.5], [1.5, 0], [3.5, 0], [5, 1.5], [5, 7]]), + V: g(ADV, [[0, 7], [2.5, 0], [5, 7]]), + W: g(8, [[0, 7], [1.5, 0], [4, 4], [6.5, 0], [8, 7]]), + X: g(ADV, [[0, 0], [5, 7]], [[0, 7], [5, 0]]), + Y: g(ADV, [[0, 7], [2.5, 3.5], [5, 7]], [[2.5, 3.5], [2.5, 0]]), + Z: g(ADV, [[0, 7], [5, 7], [0, 0], [5, 0]]), + "0": g(ADV, [[1, 6], [0, 3.5], [1, 1], [3, 0], [4.5, 1.5], [5, 3.5], [4, 6], [3, 7], [1, 6]], [[1, 1], [4, 6]]), + "1": g(4, [[1, 5.5], [2.5, 7], [2.5, 0]], [[1, 0], [4, 0]]), + "2": g(ADV, [[0, 5.5], [1, 7], [3.5, 7], [5, 5.5], [4, 3.5], [0, 0], [5, 0]]), + "3": g(ADV, [[0, 6.5], [2, 7], [4.5, 5.5], [3, 4], [4.5, 2], [3, 0], [1, 0.3]]), + "4": g(ADV, [[3.5, 0], [3.5, 7], [0, 2.5], [5, 2.5]]), + "5": g(ADV, [[5, 7], [1, 7], [0.8, 4], [3, 4.5], [4.5, 3], [3.5, 0.3], [1, 0], [0, 1]]), + "6": g(ADV, [[4.5, 6], [3, 7], [1, 6], [0, 3], [0.5, 1], [2.5, 0], [4.5, 1], [4.8, 2.8], [3, 4], [1, 3.5], [0.2, 2.5]]), + "7": g(ADV, [[0, 7], [5, 7], [2, 0]]), + "8": g(ADV, [[2.5, 4], [1, 5.5], [2.5, 7], [4, 5.5], [2.5, 4], [1, 2], [2.5, 0], [4, 2], [2.5, 4]]), + "9": g(ADV, [[4.8, 4.5], [4, 6.5], [2, 7], [0.5, 5.5], [1, 3.5], [3.5, 3], [5, 4]]), + ".": g(3, [[1, 0], [1.5, 0.4]]), + ",": g(3, [[1.5, 0.6], [0.8, -0.8]]), + ":": g(3, [[1.4, 0], [1.6, 0.4]], [[1.4, 3.5], [1.6, 3.9]]), + "·": g(3, [[1, 3.4], [1.6, 3.8]]), + "-": g(ADV, [[1, 3.5], [4, 3.5]]), + "/": g(ADV, [[0, 0], [5, 7]]), + "(": g(3, [[2.5, 7], [1, 5], [1, 2], [2.5, 0]]), + ")": g(3, [[0.5, 7], [2, 5], [2, 2], [0.5, 0]]), + "%": g(7, [[0, 0], [7, 7]], [[1, 6], [2, 7], [1, 5.5], [0, 6.5], [1, 6]], [[5, 1.5], [6, 2.5], [5, 0.5], [4, 1.5], [5, 1.5]]), + "²": g(4, [[0, 5.5], [1, 7], [2.8, 6], [1.5, 4.8], [0, 4], [3, 4]]), + "°": g(4, [[1.5, 5.5], [0.7, 6.2], [1.5, 7], [2.3, 6.2], [1.5, 5.5]]), +}; + +/** + * Zeichnet eine Zeile Text (Großschreibung) in Font-Einheiten ab (0,0) startend, + * baseline y=0, x nach rechts. Liefert Polylinien in Font-Einheiten + Gesamtbreite. + */ +export function layoutLine(text: string): { strokes: StrokePoint[][]; width: number } { + const strokes: StrokePoint[][] = []; + let cx = 0; + for (const raw of text) { + const ch = FONT[raw] ? raw : raw.toUpperCase(); + const gl = FONT[ch] ?? FONT[" "]; + for (const s of gl.strokes) { + strokes.push(s.map(([x, y]) => [x + cx, y] as StrokePoint)); + } + cx += (gl.w + 1); + } + // letzte Zusatzbreite (Seitenabstand) wieder abziehen. + const width = Math.max(0, cx - 1); + return { strokes, width }; +} + +export { CAP, ADV }; diff --git a/src/plan/toRenderScene.ts b/src/plan/toRenderScene.ts index c41b4fb..5b2eecc 100644 --- a/src/plan/toRenderScene.ts +++ b/src/plan/toRenderScene.ts @@ -9,9 +9,15 @@ import type { Plan, Primitive } from "./generatePlan"; import { applyDashRuns, buildHatchRuns } from "./glPlan/glPlanHatch"; +import { CAP, layoutLine } from "./strokeFont"; type LinePrim = Extract; +/** Modell-Meter pro Schriftpunkt (Referenz-Massstab 1:100, wie im SVG-Pfad). */ +const METERS_PER_PT = (1 / 72) * 0.0254 * 100; +/** Strichbreite der Text-Glyphen in Papier-mm (Haarlinie). */ +const TEXT_WIDTH_MM = 0.15; + export type RPoint = [number, number]; export type RRgba = [number, number, number, number]; @@ -230,8 +236,49 @@ export function planToRenderScene(plan: Plan): RScene { // Bogen als EINE zusammenhängende Polylinie (gehrte Sehnen-Ecken). const poly = tessellateArc(p.center, p.from, p.to, p.r); if (poly.length >= 2) polylines.push({ pts: poly, color: col, widthMm: p.weightMm }); + } else if (p.kind === "text") { + flushRun(); + // Raumstempel: Doc-Zeilen + Live-Zusatzzeilen als Einstrich-Glyph-Polylinien + // (Modell-Meter, vertikal um den Anker zentriert). Erste Fassung: Versalien. + const col = toRgba(p.color, 1) ?? [0.1, 0.1, 0.1, 1]; + const docLines = p.doc.paragraphs.map((par) => ({ + text: par.runs.map((r) => r.text).join(""), + align: par.align ?? "left", + sizePt: p.basePt, + })); + const extraLines = p.extraLines.map((t) => ({ + text: t, + align: "center" as const, + sizePt: p.basePt * 0.8, + })); + const allLines = [...docLines, ...extraLines].filter((l) => l.text.length > 0); + if (allLines.length > 0) { + const gaps = allLines.map((l) => l.sizePt * METERS_PER_PT * 1.3); + const totalH = gaps.reduce((a, b) => a + b, 0); + let cursorY = p.at.y + totalH / 2; + allLines.forEach((line, i) => { + const scale = (line.sizePt * METERS_PER_PT * 0.7) / CAP; + cursorY -= gaps[i]; + const baseY = cursorY; + const { strokes, width } = layoutLine(line.text); + const wModel = width * scale; + const startX = + line.align === "center" + ? p.at.x - wModel / 2 + : line.align === "right" + ? p.at.x - wModel + : p.at.x; + for (const s of strokes) { + if (s.length < 2) continue; + polylines.push({ + pts: s.map(([x, y]) => [startX + x * scale, baseY + y * scale] as RPoint), + color: col, + widthMm: TEXT_WIDTH_MM, + }); + } + }); + } } else { - // "text" wird bewusst übersprungen (render2d rendert keinen Text). flushRun(); } }