Nativer 2D-Viewport: Raumstempel-Text (Einstrich-Vektorschrift)
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.
This commit is contained in:
@@ -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<string, Glyph> = {
|
||||||
|
" ": 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 };
|
||||||
@@ -9,9 +9,15 @@
|
|||||||
|
|
||||||
import type { Plan, Primitive } from "./generatePlan";
|
import type { Plan, Primitive } from "./generatePlan";
|
||||||
import { applyDashRuns, buildHatchRuns } from "./glPlan/glPlanHatch";
|
import { applyDashRuns, buildHatchRuns } from "./glPlan/glPlanHatch";
|
||||||
|
import { CAP, layoutLine } from "./strokeFont";
|
||||||
|
|
||||||
type LinePrim = Extract<Primitive, { kind: "line" }>;
|
type LinePrim = Extract<Primitive, { kind: "line" }>;
|
||||||
|
|
||||||
|
/** 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 RPoint = [number, number];
|
||||||
export type RRgba = [number, number, 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).
|
// Bogen als EINE zusammenhängende Polylinie (gehrte Sehnen-Ecken).
|
||||||
const poly = tessellateArc(p.center, p.from, p.to, p.r);
|
const poly = tessellateArc(p.center, p.from, p.to, p.r);
|
||||||
if (poly.length >= 2) polylines.push({ pts: poly, color: col, widthMm: p.weightMm });
|
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 {
|
} else {
|
||||||
// "text" wird bewusst übersprungen (render2d rendert keinen Text).
|
|
||||||
flushRun();
|
flushRun();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user