2D-Engine-Parität: WASM-Pfad rendert deckungsgleich zum SVG-Referenzpfad
Sechs Lücken geschlossen: Text-Massstab vom Geometrie-Massstab entkoppelt (set_text_scale, spiegelt SVG-Referenzskala); glyphon auf ColorMode::Web (Text war linear-konvertiert zu dunkel); z-basierte Maler-Reihenfolge (interleavte draw_sequence statt fills-vor-lines, Alt-Szenen unverändert); CSS-Klassenfarben/-Opacities in toRenderScene gespiegelt (Türschwenk etc.); greyed-Dimmung 0.3 auf allen Primitiven; Dämmschraffur am Modell-Ursprung verankert (userSpaceOnUse), exakte Bézier-Wellenform statt Sinus und kachelgekoppelte Strichbreite (widthScreen-Modus). Probe scripts/probe-engine-parity.mjs vergleicht ?gl=0 gegen ?engine=wasm; Rest-Diff nur AA/Glyphen-Rasterung. cargo 17/17, vitest 94/94, Builds grün.
This commit is contained in:
+15
-3
@@ -475,11 +475,23 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
const paperScaleForGl = (v: ViewBox): number =>
|
||||
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
|
||||
|
||||
// Referenz-Nenner (1:N) NUR für die Text-Schriftgröße der Engine — spiegelt
|
||||
// EXAKT die SVG-Formel (case "text" unten, gleiche `print`-Bedingung): im
|
||||
// normalen Anzeigemodus fest auf die Referenzskala 1:100 (Raumstempel bleibt
|
||||
// lesbar, unabhängig vom zufälligen Einpass-Massstab der Ansicht), erst im
|
||||
// Druck-Modus der echte gemessene `paperScale`. BEWUSST NICHT `paperScaleForGl`
|
||||
// (das dort verwendete live abgeleitete N wäre für Text willkürlich/unlesbar
|
||||
// und würde vom SVG-Referenzpfad abweichen — siehe Doku in `gpu::Renderer`).
|
||||
const textScaleForGl = (): number => {
|
||||
const print = !hairline && paperScale != null && paperScale > 0;
|
||||
return print ? paperScale! : 100;
|
||||
};
|
||||
|
||||
// GL-Geometrie beim Plan-Wechsel neu tessellieren + sofort neu zeichnen.
|
||||
useEffect(() => {
|
||||
if (!useGpuRenderer) return;
|
||||
updateGeometry(plan);
|
||||
renderGl(viewRef.current, paperScaleForGl(viewRef.current));
|
||||
renderGl(viewRef.current, paperScaleForGl(viewRef.current), textScaleForGl());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [useGpuRenderer, plan, updateGeometry, renderGl]);
|
||||
|
||||
@@ -487,7 +499,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
// keine Re-Tessellierung. Auch bei paperScale-Wechsel neu (Strichbreiten).
|
||||
useEffect(() => {
|
||||
if (useGpuRenderer) {
|
||||
renderGl(view, paperScaleForGl(view));
|
||||
renderGl(view, paperScaleForGl(view), textScaleForGl());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [useGpuRenderer, renderGl, view, paperScale]);
|
||||
@@ -1065,7 +1077,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
if (!v || !v0) return;
|
||||
viewRef.current = v; // abgeleitete Reads (Snap-Toleranz etc.) frisch halten
|
||||
applySvgTransform(v0, v); // SVG per GPU-Composit (kein Re-Raster)
|
||||
renderGl(v, paperScaleForGl(v)); // GL exakt auf V (Strich in echten Papier-mm)
|
||||
renderGl(v, paperScaleForGl(v), textScaleForGl()); // GL exakt auf V (Strich in echten Papier-mm)
|
||||
};
|
||||
/** Pan-/Zoom-Ziel imperativ anwenden (rAF-gedrosselt), ohne React-Render. */
|
||||
const enqueuePan = (v: ViewBox) => {
|
||||
|
||||
@@ -240,10 +240,12 @@ export function hatchLineFamily(
|
||||
const nrm: Vec2 = { x: -dir.y, y: dir.x }; // Linien-Normale (Scan-Richtung)
|
||||
|
||||
// Rechtwinklige Projektion aller Ecken auf die Normale → Abdeckungsbereich.
|
||||
const center: Vec2 = {
|
||||
x: (bb.minX + bb.maxX) / 2,
|
||||
y: (bb.minY + bb.maxY) / 2,
|
||||
};
|
||||
// ANKER = MODELL-URSPRUNG (nicht die Polygon-Mitte): der SVG-`<pattern>` ist
|
||||
// `userSpaceOnUse`, sein Kachelgitter liegt global im viewBox-Raum fest
|
||||
// (Ursprung (0,0) = Modell-Ursprung). Nur mit demselben Anker liegen die
|
||||
// Musterlinien deckungsgleich zum SVG-Referenzpfad — und benachbarte Polygone
|
||||
// teilen dieselbe Muster-Phase statt je eigener (bbox-abhängiger) Versätze.
|
||||
const center: Vec2 = { x: 0, y: 0 };
|
||||
let dMin = Infinity, dMax = -Infinity;
|
||||
for (const p of poly) {
|
||||
const d = (p.x - center.x) * nrm.x + (p.y - center.y) * nrm.y;
|
||||
@@ -415,24 +417,24 @@ export function buildHatchRuns(poly: Vec2[], hatch: HatchRender): HatchRun[] {
|
||||
* endet exakt an den Polygonkanten (even-odd-Clipping, konkav-fähig).
|
||||
*/
|
||||
function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchRun[] {
|
||||
const bb = bbox(poly);
|
||||
const rot = toModelAngleRad(hatch.angle);
|
||||
|
||||
// Muster-Parameter in Metern (aus den SVG-px-Werten der Kachel).
|
||||
const wavelength = 14 * scale * PX_TO_M; // Kachelbreite
|
||||
const rowGap = 10 * scale * PX_TO_M; // Kachelhöhe → Zeilenabstand
|
||||
const amplitude = 3 * scale * PX_TO_M; // Wellen-Amplitude (weich)
|
||||
|
||||
// Lokales Muster-Koordinatensystem: u entlang der Welle, v quer (Zeilen).
|
||||
// ANKER = MODELL-URSPRUNG, wie beim SVG-`userSpaceOnUse`-Pattern (siehe
|
||||
// hatchLineFamily): so liegen Wellen-Phase UND Zeilenraster deckungsgleich
|
||||
// zum SVG-Referenzpfad und über alle Polygone hinweg in EINEM Gitter.
|
||||
const uDir = rotate({ x: 1, y: 0 }, rot);
|
||||
const vDir = rotate({ x: 0, y: 1 }, rot);
|
||||
|
||||
// Abdeckungsbereich in (u,v) über die Polygon-Ecken bestimmen.
|
||||
const center: Vec2 = { x: (bb.minX + bb.maxX) / 2, y: (bb.minY + bb.maxY) / 2 };
|
||||
let uMin = Infinity, uMax = -Infinity, vMin = Infinity, vMax = -Infinity;
|
||||
for (const p of poly) {
|
||||
const u = (p.x - center.x) * uDir.x + (p.y - center.y) * uDir.y;
|
||||
const v = (p.x - center.x) * vDir.x + (p.y - center.y) * vDir.y;
|
||||
const u = p.x * uDir.x + p.y * uDir.y;
|
||||
const v = p.x * vDir.x + p.y * vDir.y;
|
||||
if (u < uMin) uMin = u;
|
||||
if (u > uMax) uMax = u;
|
||||
if (v < vMin) vMin = v;
|
||||
@@ -447,22 +449,38 @@ function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchR
|
||||
|
||||
const runs: HatchRun[] = [];
|
||||
const toWorld = (u: number, v: number): Vec2 => ({
|
||||
x: center.x + uDir.x * u + vDir.x * v,
|
||||
y: center.y + uDir.y * u + vDir.y * v,
|
||||
x: uDir.x * u + vDir.x * v,
|
||||
y: uDir.y * u + vDir.y * v,
|
||||
});
|
||||
|
||||
const firstRow = Math.floor(vMin / rowGap) - 1;
|
||||
const lastRow = Math.ceil(vMax / rowGap) + 1;
|
||||
// Die Wellen-Mittellinie liegt in der SVG-Kachel bei y = h/2 (my = 5·scale),
|
||||
// die Zeilen also auf HALBEN Rasterlinien: v = (r + 0.5)·rowGap.
|
||||
const firstRow = Math.floor(vMin / rowGap - 0.5) - 1;
|
||||
const lastRow = Math.ceil(vMax / rowGap - 0.5) + 1;
|
||||
const uStart = Math.floor((uMin - wavelength) / du) * du;
|
||||
const uEnd = uMax + wavelength;
|
||||
|
||||
// EXAKT die SVG-Wellenform: die Kachel zeichnet zwei quadratische Béziers
|
||||
// `M0 5s Q 3.5s -1s 7s 5s Q 10.5s 11s 14s 5s`. Deren x(t) ist LINEAR (7s·t je
|
||||
// Halbwelle), die Auslenkung eine Parabel ±12s·t(1−t) (Scheitel ±3s) — also
|
||||
// Halbwellen-Parabeln statt einer Sinuskurve (flachere Kämme, steilere
|
||||
// Nulldurchgänge). Erste Halbwelle: Kamm nach OBEN (Bildschirm) = +v (Modell).
|
||||
const half = wavelength / 2;
|
||||
const waveAt = (u: number): number => {
|
||||
const phase = ((u % wavelength) + wavelength) % wavelength;
|
||||
const t = (phase % half) / half;
|
||||
const mag = 12 * scale * PX_TO_M * t * (1 - t);
|
||||
return phase < half ? mag : -mag;
|
||||
};
|
||||
|
||||
for (let r = firstRow; r <= lastRow; r++) {
|
||||
const vBase = r * rowGap;
|
||||
const vBase = (r + 0.5) * rowGap;
|
||||
// Ganze Welle als EINE Polylinie tessellieren, dann DURCHGEHEND clippen.
|
||||
// Das u-Raster ist an Vielfachen von du = λ/22 ausgerichtet → die Null-
|
||||
// durchgänge (Vielfache von λ/2 = 11·du) liegen exakt auf Stützpunkten.
|
||||
const wavePts: Vec2[] = [];
|
||||
for (let u = uStart; u <= uEnd + 1e-9; u += du) {
|
||||
const wave = amplitude * Math.sin((2 * Math.PI * u) / wavelength);
|
||||
wavePts.push(toWorld(u, vBase + wave));
|
||||
wavePts.push(toWorld(u, vBase + waveAt(u)));
|
||||
}
|
||||
for (const run of clipPolylineToPolygon(wavePts, poly)) runs.push(run);
|
||||
}
|
||||
|
||||
+107
-20
@@ -31,18 +31,36 @@ export type RRgba = [number, number, number, number];
|
||||
export interface RFill {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
/**
|
||||
* Maler-Reihenfolge (kleiner = früher/unten). Der SVG-Pfad zeichnet die
|
||||
* Primitive strikt in Array-Reihenfolge (Füllung/Schraffur/Umriss
|
||||
* interleaved) — eine spätere Wand-Poché deckt z. B. den früheren
|
||||
* Raum-Umriss ab. Der native Renderer (`compile_scene`) stellt diese
|
||||
* Reihenfolge über die getrennten Szenen-Arrays hinweg wieder her.
|
||||
*/
|
||||
z: number;
|
||||
}
|
||||
export interface ROutline {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
dash?: number[] | null;
|
||||
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||
z: number;
|
||||
}
|
||||
export interface RPolyline {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
dash?: number[] | null;
|
||||
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||
z: number;
|
||||
/**
|
||||
* true: `widthMm` trägt BILDSCHIRM-Einheiten (viewBox-px, zoom-skalierend)
|
||||
* statt Papier-mm — für Schraffur-Striche, deren Breite im SVG an die
|
||||
* Musterkachel gekoppelt ist (`hatchStrokePx`), nicht an Papier-mm.
|
||||
*/
|
||||
widthScreen?: boolean;
|
||||
}
|
||||
export interface RArc {
|
||||
center: RPoint;
|
||||
@@ -59,6 +77,8 @@ export interface RLine {
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
dash?: number[] | null;
|
||||
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||
z: number;
|
||||
}
|
||||
export type RTextAlign = "left" | "center" | "right";
|
||||
/** EINE Textzeile; serde-kompatibel zu render2d::types::Text. */
|
||||
@@ -82,6 +102,41 @@ export interface RScene {
|
||||
|
||||
const DEFAULT_LINE = "#1a1a1a";
|
||||
|
||||
/**
|
||||
* Strichfarben/-deckkraft je CSS-Klasse — Spiegel der `.plan-svg .<cls>`-Regeln
|
||||
* in styles.css: Im SVG-Pfad ÜBERSTIMMT eine CSS-`stroke`-Property das inline
|
||||
* gesetzte `stroke`-Attribut (Präsentationsattribut), d. h. für diese Klassen
|
||||
* gilt IMMER die CSS-Farbe. Klassen ohne CSS-`stroke` (draw2d, stair-*,
|
||||
* context-line) behalten die Modell-Farbe; context-line trägt zusätzlich
|
||||
* `opacity: 0.7`, wall-axis `opacity: 0.85`.
|
||||
*/
|
||||
const CLS_STROKE: Record<string, string> = {
|
||||
"door-leaf": "#2b3039",
|
||||
"door-swing": "#6b7280",
|
||||
"door-frame": "#2b3039",
|
||||
"window-glass": "#4a86c7",
|
||||
"wall-axis": "#2f5d54", // var(--accent) des hellen Standard-Themes
|
||||
};
|
||||
const CLS_OPACITY: Record<string, number> = {
|
||||
"wall-axis": 0.85,
|
||||
"context-line": 0.7,
|
||||
};
|
||||
|
||||
/** Deckkraft gedimmter (greyed) Primitive — wie GREYED_OPACITY in PlanView. */
|
||||
const GREYED_ALPHA = 0.3;
|
||||
|
||||
/** Farbe mit Klassen-Deckkraft und Greyed-Dimmung multiplizieren. */
|
||||
function withOpacity(col: RRgba, cls: string | undefined, greyed: boolean | undefined): RRgba {
|
||||
const o = (cls ? CLS_OPACITY[cls] ?? 1 : 1) * (greyed ? GREYED_ALPHA : 1);
|
||||
return o === 1 ? col : [col[0], col[1], col[2], col[3] * o];
|
||||
}
|
||||
|
||||
/** Strichfarbe einer line/arc-Primitive: CSS-Klassen-Farbe > Modell-Farbe > Tinte. */
|
||||
function strokeColorFor(cls: string | undefined, color: string | undefined): RRgba {
|
||||
const css = cls ? CLS_STROKE[cls] : undefined;
|
||||
return toRgba(css ?? color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
}
|
||||
|
||||
/** 1 Punkt (pt) in Papier-Millimetern. */
|
||||
const PT_TO_MM = 25.4 / 72;
|
||||
/**
|
||||
@@ -241,29 +296,36 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
// Sichtbare Umriss-Läufe werden erst GESAMMELT (nach Stil gruppiert) und am
|
||||
// Ende über Polygon-Grenzen hinweg zusammengenäht — erst dadurch bekommen
|
||||
// Wand-zu-Wand-Ecken (zwei Polygone!) eine Gehrung statt einer Kerbe.
|
||||
const edgeRunGroups = new Map<string, { color: RRgba; widthMm: number; runs: RPoint[][] }>();
|
||||
const collectEdgeRun = (run: RPoint[], color: RRgba, widthMm: number) => {
|
||||
const edgeRunGroups = new Map<string, { color: RRgba; widthMm: number; runs: RPoint[][]; z: number }>();
|
||||
const collectEdgeRun = (run: RPoint[], color: RRgba, widthMm: number, z: number) => {
|
||||
const k = `${color.join(",")}|${widthMm}`;
|
||||
let g = edgeRunGroups.get(k);
|
||||
if (!g) {
|
||||
g = { color, widthMm, runs: [] };
|
||||
g = { color, widthMm, runs: [], z };
|
||||
edgeRunGroups.set(k, g);
|
||||
}
|
||||
g.runs.push(run);
|
||||
// Die Kette zeichnet, wenn ihr SPÄTESTER Beitrag im SVG zeichnen würde.
|
||||
g.z = Math.max(g.z, z);
|
||||
};
|
||||
|
||||
// Maler-Reihenfolge: läuft in EXAKT der Reihenfolge mit, in der der SVG-Pfad
|
||||
// die Elemente zeichnet (Primitive in Array-Reihenfolge; je Polygon Füllung →
|
||||
// Schraffur → Umriss). Jedes emittierte Szenen-Element bekommt das nächste z.
|
||||
let zc = 0;
|
||||
|
||||
// Laufender, verketteter 2D-Zeichnungs-Zug (gleiche drawingId + Stil, End-an-Start).
|
||||
let curPts: RPoint[] | null = null;
|
||||
let curSrc: LinePrim | null = null;
|
||||
const flushRun = () => {
|
||||
if (curPts && curSrc && curPts.length >= 2) {
|
||||
const col = toRgba(curSrc.color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
const col = withOpacity(strokeColorFor(curSrc.cls, curSrc.color), curSrc.cls, curSrc.greyed);
|
||||
const closed = curPts.length > 2 && samePt(curPts[0], curPts[curPts.length - 1]);
|
||||
const dash = curSrc.dash ?? null;
|
||||
if (closed) {
|
||||
outlines.push({ pts: curPts.slice(0, -1), color: col, widthMm: curSrc.weightMm, dash });
|
||||
outlines.push({ pts: curPts.slice(0, -1), color: col, widthMm: curSrc.weightMm, dash, z: zc++ });
|
||||
} else {
|
||||
polylines.push({ pts: curPts, color: col, widthMm: curSrc.weightMm, dash });
|
||||
polylines.push({ pts: curPts, color: col, widthMm: curSrc.weightMm, dash, z: zc++ });
|
||||
}
|
||||
}
|
||||
curPts = null;
|
||||
@@ -278,33 +340,53 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
|
||||
// Füllung
|
||||
const fillCol = toRgba(p.fill, 1);
|
||||
if (fillCol && pts.length >= 3) fills.push({ pts, color: fillCol });
|
||||
if (fillCol && pts.length >= 3) {
|
||||
fills.push({ pts, color: withOpacity(fillCol, undefined, p.greyed), z: zc++ });
|
||||
}
|
||||
|
||||
// Schraffur: die aufs Polygon geclippten Musterlinien wie im Browser
|
||||
// (glPlanHatch — identische Geometrie), als Polylinien. "none"/"solid"
|
||||
// brauchen keine Linien (solid deckt die Füllung farbig ab).
|
||||
if (p.hatch && p.hatch.pattern !== "none" && p.hatch.pattern !== "solid") {
|
||||
const hatchCol = toRgba(p.hatch.color, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
const hatchCol = withOpacity(
|
||||
toRgba(p.hatch.color, 1) ?? [0.1, 0.1, 0.1, 1],
|
||||
undefined,
|
||||
p.greyed,
|
||||
);
|
||||
// Strichbreite wie das SVG-Muster (`hatchStrokePx`): in viewBox-px, an
|
||||
// die Kachel gekoppelt und mit dem Zoom skalierend — NICHT Papier-mm
|
||||
// (sonst wird die Schraffur bei kleinen Masstäben zur Haarlinie).
|
||||
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
|
||||
const hatchPx = Math.max(0.6, hatchMm * (1 / 0.13));
|
||||
const runs = applyDashRuns(buildHatchRuns(p.pts, p.hatch), p.hatch.dash);
|
||||
// Alle Musterläufe eines Polygons teilen EIN z (überlappen einander nicht).
|
||||
const hatchZ = zc++;
|
||||
for (const run of runs) {
|
||||
if (run.length >= 2) {
|
||||
polylines.push({ pts: run.map((v) => [v.x, v.y]), color: hatchCol, widthMm: hatchMm });
|
||||
polylines.push({
|
||||
pts: run.map((v) => [v.x, v.y]),
|
||||
color: hatchCol,
|
||||
widthMm: hatchPx,
|
||||
widthScreen: true,
|
||||
z: hatchZ,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Umriss
|
||||
const strokeCol = toRgba(p.stroke, 1);
|
||||
if (strokeCol && p.strokeWidthMm > 0) {
|
||||
const strokeColRaw = toRgba(p.stroke, 1);
|
||||
if (strokeColRaw && p.strokeWidthMm > 0) {
|
||||
const strokeCol = withOpacity(strokeColRaw, undefined, p.greyed);
|
||||
const suppressed = p.noStrokeEdges ?? [];
|
||||
if (suppressed.length === 0) {
|
||||
// Ganzer Ring → geschlossener, gehrter Umriss.
|
||||
outlines.push({ pts, color: strokeCol, widthMm: p.strokeWidthMm });
|
||||
outlines.push({ pts, color: strokeCol, widthMm: p.strokeWidthMm, z: zc++ });
|
||||
} else {
|
||||
// Nur die sichtbaren Kanten — sammeln, die Naht ans Ende verschoben.
|
||||
const runZ = zc++;
|
||||
for (const run of visibleEdgeRuns(pts, suppressed)) {
|
||||
collectEdgeRun(run, strokeCol, p.strokeWidthMm);
|
||||
collectEdgeRun(run, strokeCol, p.strokeWidthMm, runZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,12 +405,13 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
} else {
|
||||
// Genuin einzelnes Segment (Türblatt, Referenzlinie, Symbol) → Stumpfkappen ok.
|
||||
flushRun();
|
||||
const col = toRgba(p.color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
lines.push({ a: [p.a.x, p.a.y], b: [p.b.x, p.b.y], color: col, widthMm: p.weightMm, dash: p.dash ?? null });
|
||||
const col = withOpacity(strokeColorFor(p.cls, p.color), p.cls, p.greyed);
|
||||
lines.push({ a: [p.a.x, p.a.y], b: [p.b.x, p.b.y], color: col, widthMm: p.weightMm, dash: p.dash ?? null, z: zc++ });
|
||||
}
|
||||
} else if (p.kind === "arc") {
|
||||
flushRun();
|
||||
const col = toRgba(DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
// Farbe wie im SVG: CSS-Klassen-Regel (z. B. .door-swing → #6b7280).
|
||||
const col = withOpacity(strokeColorFor(p.cls, undefined), p.cls, p.greyed);
|
||||
// Bogen unvortessellliert an Rust übergeben (`RArc`) — die exakte runde
|
||||
// Darstellung (analytischer SDF-Shader, `ARC_WGSL`) und ein evtl. Strich-
|
||||
// muster übernimmt der native Renderer (`compile_scene`) drüben.
|
||||
@@ -348,7 +431,7 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
// "text"): Rich-Text-Zeilen + Live-Zusatzzeilen, Block vertikal um den
|
||||
// Anker zentriert, Zeilenvorschub 1.3·Basisgröße.
|
||||
flushRun();
|
||||
const defCol = toRgba(p.color, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
const defCol = withOpacity(toRgba(p.color, 1) ?? [0.1, 0.1, 0.1, 1], undefined, p.greyed);
|
||||
// unitPerPt = PT_TO_MM → tspan.fontSize ist direkt die Papier-mm-Größe.
|
||||
const docLines = docToLines(p.doc, {
|
||||
x: 0,
|
||||
@@ -368,7 +451,11 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
sizeMm: line.tspans.length
|
||||
? Math.max(...line.tspans.map((t) => t.fontSize))
|
||||
: baseMm,
|
||||
color: toRgba(line.tspans[0]?.fill ?? p.color, 1) ?? defCol,
|
||||
color: withOpacity(
|
||||
toRgba(line.tspans[0]?.fill ?? p.color, 1) ?? defCol,
|
||||
undefined,
|
||||
p.greyed,
|
||||
),
|
||||
align: line.align,
|
||||
});
|
||||
}
|
||||
@@ -402,9 +489,9 @@ export function planToRenderScene(plan: Plan): RScene {
|
||||
for (const chain of stitchRuns(g.runs)) {
|
||||
if (chain.pts.length < 2) continue;
|
||||
if (chain.closed) {
|
||||
outlines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm });
|
||||
outlines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm, z: g.z });
|
||||
} else {
|
||||
polylines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm });
|
||||
polylines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm, z: g.z });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Plan } from "./generatePlan";
|
||||
import { planToRenderScene } from "./toRenderScene";
|
||||
// wgpu-22-Limit-Shim (maxInterStageShaderComponents), geteilt mit useWasm3dRenderer.
|
||||
import { patchRequestDeviceLimits } from "../engine/requestDeviceShim";
|
||||
|
||||
interface ViewBox {
|
||||
x: number;
|
||||
@@ -23,39 +25,12 @@ interface ViewBox {
|
||||
h: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompatibilitäts-Shim: wgpu 22 sendet in `requestDevice` noch das Limit
|
||||
* `maxInterStageShaderComponents`, das AUS DER WebGPU-Spec ENTFERNT wurde. Neuere
|
||||
* Chromium-Builds lehnen ein nicht-`undefined`-Wert dafür mit `OperationError`
|
||||
* ab (→ Device-Erzeugung schlägt fehl). Wir entfernen den Schlüssel EINMAL global
|
||||
* aus dem Descriptor, bevor wgpu ihn stellt. Entfällt, sobald render2d auf ein
|
||||
* wgpu (+ glyphon) hochgezogen wird, das dieses Limit nicht mehr mitschickt.
|
||||
*/
|
||||
let requestDevicePatched = false;
|
||||
function patchRequestDeviceLimits(): void {
|
||||
if (requestDevicePatched) return;
|
||||
const AdapterCtor = (globalThis as any).GPUAdapter;
|
||||
if (!AdapterCtor?.prototype?.requestDevice) return;
|
||||
const orig = AdapterCtor.prototype.requestDevice;
|
||||
AdapterCtor.prototype.requestDevice = function (
|
||||
this: unknown,
|
||||
desc?: any,
|
||||
) {
|
||||
if (desc?.requiredLimits && "maxInterStageShaderComponents" in desc.requiredLimits) {
|
||||
const requiredLimits = { ...desc.requiredLimits };
|
||||
delete requiredLimits.maxInterStageShaderComponents;
|
||||
desc = { ...desc, requiredLimits };
|
||||
}
|
||||
return orig.call(this, desc);
|
||||
};
|
||||
requestDevicePatched = true;
|
||||
}
|
||||
|
||||
/** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine`). */
|
||||
interface WebPlanRendererLike {
|
||||
set_scene(json: string): void;
|
||||
set_view_box(x: number, y: number, w: number, h: number): void;
|
||||
set_paper_scale(n: number): void;
|
||||
set_text_scale(n: number): void;
|
||||
resize(w: number, h: number): void;
|
||||
render(): void;
|
||||
}
|
||||
@@ -89,7 +64,7 @@ function getRenderer(canvas: HTMLCanvasElement): Promise<WebPlanRendererLike> {
|
||||
let p = rendererCache.get(canvas);
|
||||
if (!p) {
|
||||
p = (async () => {
|
||||
patchRequestDeviceLimits(); // wgpu-22-Limit-Shim (siehe oben)
|
||||
patchRequestDeviceLimits(); // wgpu-22-Limit-Shim (src/engine/requestDeviceShim)
|
||||
const mod = await loadEngine();
|
||||
// Canvas VOR dem Surface-Erzeugen auf die Zielpixel (DPR) bringen.
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
@@ -149,9 +124,15 @@ export function useWasmPlanRenderer(
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** Einen Frame zeichnen (Canvas-Größe + dpr hier abgleichen). */
|
||||
/**
|
||||
* Einen Frame zeichnen (Canvas-Größe + dpr hier abgleichen). `textScaleN`
|
||||
* bewusst getrennt von `paperScaleN`: er treibt NUR die Schriftgröße der
|
||||
* Raumstempel (spiegelt die SVG-Formel — feste Referenzskala 1:100 im
|
||||
* Anzeigemodus, siehe PlanView.tsx `textScaleForGl`), während `paperScaleN`
|
||||
* weiter die echten Papier-mm-Strichbreiten der Geometrie treibt.
|
||||
*/
|
||||
const render = useCallback(
|
||||
(viewBox: ViewBox, paperScaleN = 100) => {
|
||||
(viewBox: ViewBox, paperScaleN = 100, textScaleN = 100) => {
|
||||
const r = rendererRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!r || !canvas) return;
|
||||
@@ -166,6 +147,7 @@ export function useWasmPlanRenderer(
|
||||
try {
|
||||
r.resize(w, h);
|
||||
r.set_paper_scale(paperScaleN);
|
||||
r.set_text_scale(textScaleN);
|
||||
r.set_view_box(viewBox.x, viewBox.y, viewBox.w, viewBox.h);
|
||||
r.render();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user