Native wgpu-Viewports (2D+3D) im Tauri-Prozess: echtes Modell + gehrte Ecken
- native.rs: EINE winit-EventLoop hostet 2D- und 3D-Fenster (winit erlaubt nur eine Loop pro Prozess) — loest den RecreationAttempt-Panic zweier Loops; ersetzt native2d.rs/native3d.rs. Feature-gegated (native2d/native3d, einzeln oder zusammen). - render2d/render3d laden das ECHTE Modell aus assets/native2d_scene.json bzw. native3d_walls.json (Demo-Szene als Fallback); initialer Ausschnitt/Kamera aus den Modell-Grenzen gerahmt, initialer Redraw + gesetzte Fenstergroesse. - TS-Konverter toRenderScene/toWalls3d + scripts/dump-native-scene erzeugen die JSON aus sampleProject/generatePlan (npm run dump:native). - render2d: Scene.polylines fuer zusammenhaengende Umriss-/Zeichnungslaeufe → Gehrung statt Stumpfkappen an Wandecken/2D-Geometrien; MITER_LIMIT 4→8 (deckungsgleich mit SVG stroke-miterlimit:8 und WebGL2). - native3d-Feature + render3d-Pfad-Dep in der Tauri-Crate.
This commit is contained in:
@@ -174,8 +174,10 @@ export function compilePrimitivesToGpu(
|
||||
else lineBatches.push({ startIndex: lineIdx.length - count, indexCount: count, color, strokeMm });
|
||||
};
|
||||
|
||||
/** Maximaler Miter-Längenfaktor; darüber wird geklemmt (kein Spike an spitzen Ecken). */
|
||||
const MITER_LIMIT = 4;
|
||||
/** Maximaler Miter-Längenfaktor; darüber wird geklemmt (kein Spike an spitzen Ecken).
|
||||
* 8 entspricht `stroke-miterlimit: 8` im SVG-Pfad (styles.css) und dem nativen
|
||||
* render2d (tessellate.rs) — so sehen spitze Ecken in allen drei Pfaden gleich aus. */
|
||||
const MITER_LIMIT = 8;
|
||||
|
||||
/**
|
||||
* Zeichnet einen zusammenhängenden Linienzug (Bildschirm-Raum) als EINEN
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
// Flacht einen {@link Plan} (die eine Wahrheit aus generatePlan) auf das
|
||||
// GPU-nahe Szenenformat des nativen render2d-Renderers ab: Füllpolygone,
|
||||
// Umrisse und Linien. Hatch und Text bleiben (wie im Rust-Renderer, siehe
|
||||
// render2d/src/types.rs) vorerst außen vor — sie sind Overlay/späteres M3.
|
||||
//
|
||||
// Das erzeugte Objekt matcht 1:1 die serde-Structs render2d::types::Scene
|
||||
// { fills:[{pts,color}], outlines:[{pts,color,widthMm}], lines:[{a,b,color,widthMm}] }
|
||||
// mit Point = [x,y] (Meter) und Rgba = [r,g,b,a] (0..1).
|
||||
|
||||
import type { Plan, Primitive } from "./generatePlan";
|
||||
|
||||
type LinePrim = Extract<Primitive, { kind: "line" }>;
|
||||
|
||||
export type RPoint = [number, number];
|
||||
export type RRgba = [number, number, number, number];
|
||||
|
||||
export interface RFill {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
}
|
||||
export interface ROutline {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
}
|
||||
export interface RPolyline {
|
||||
pts: RPoint[];
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
}
|
||||
export interface RLine {
|
||||
a: RPoint;
|
||||
b: RPoint;
|
||||
color: RRgba;
|
||||
widthMm: number;
|
||||
}
|
||||
export interface RScene {
|
||||
fills: RFill[];
|
||||
outlines: ROutline[];
|
||||
polylines: RPolyline[];
|
||||
lines: RLine[];
|
||||
}
|
||||
|
||||
const DEFAULT_LINE = "#1a1a1a";
|
||||
|
||||
/** Wenige benannte Farben, die in Klassen/Defaults vorkommen können. */
|
||||
const NAMED: Record<string, string> = {
|
||||
black: "#000000",
|
||||
white: "#ffffff",
|
||||
red: "#ff0000",
|
||||
none: "none",
|
||||
transparent: "none",
|
||||
};
|
||||
|
||||
/** Hex/Named-Farbe → [r,g,b,a] in 0..1, oder null bei "none"/leer. */
|
||||
function toRgba(input: string | undefined, alpha = 1): RRgba | null {
|
||||
if (!input) return null;
|
||||
let h = input.trim().toLowerCase();
|
||||
if (h === "none" || h === "transparent") return null;
|
||||
if (h[0] !== "#") {
|
||||
const mapped = NAMED[h];
|
||||
if (!mapped) return [0, 0, 0, alpha];
|
||||
if (mapped === "none") return null;
|
||||
h = mapped;
|
||||
}
|
||||
h = h.slice(1);
|
||||
// Kurzformen auf Langform expandieren (#rgb → #rrggbb, #rgba → #rrggbbaa).
|
||||
if (h.length === 3 || h.length === 4) h = h.split("").map((c) => c + c).join("");
|
||||
if (h.length !== 6 && h.length !== 8) return [0, 0, 0, alpha];
|
||||
const r = parseInt(h.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(h.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(h.slice(4, 6), 16) / 255;
|
||||
// 8-stelliges Hex trägt den Alpha-Kanal selbst; sonst der Parameter.
|
||||
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : alpha;
|
||||
if (![r, g, b, a].every((v) => Number.isFinite(v))) return [0, 0, 0, alpha];
|
||||
return [r, g, b, a];
|
||||
}
|
||||
|
||||
/** Kreisbogen in Liniensegmente zerlegen (kürzerer Sweep, ~24 Segmente). */
|
||||
function tessellateArc(
|
||||
center: { x: number; y: number },
|
||||
from: { x: number; y: number },
|
||||
to: { x: number; y: number },
|
||||
r: number,
|
||||
): RPoint[] {
|
||||
const a0 = Math.atan2(from.y - center.y, from.x - center.x);
|
||||
let a1 = Math.atan2(to.y - center.y, to.x - center.x);
|
||||
// Kürzeren Bogen wählen (generatePlan liefert keine largeArc-Info mit).
|
||||
let delta = a1 - a0;
|
||||
while (delta > Math.PI) delta -= 2 * Math.PI;
|
||||
while (delta < -Math.PI) delta += 2 * Math.PI;
|
||||
a1 = a0 + delta;
|
||||
const segs = Math.max(2, Math.ceil((Math.abs(delta) / (Math.PI * 2)) * 48));
|
||||
const pts: RPoint[] = [];
|
||||
for (let i = 0; i <= segs; i++) {
|
||||
const t = a0 + (delta * i) / segs;
|
||||
pts.push([center.x + Math.cos(t) * r, center.y + Math.sin(t) * r]);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
/** Zwei Modell-Punkte (Meter) als gleich behandeln (Verkettungs-Toleranz). */
|
||||
function samePt(a: RPoint, b: RPoint): boolean {
|
||||
return Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
|
||||
}
|
||||
|
||||
/** Gleicher Linienstil (für das Verketten aufeinanderfolgender 2D-Zeichenlinien). */
|
||||
function sameLineStyle(a: LinePrim, b: LinePrim): boolean {
|
||||
return (
|
||||
a.cls === b.cls &&
|
||||
a.weightMm === b.weightMm &&
|
||||
(a.color ?? "") === (b.color ?? "") &&
|
||||
JSON.stringify(a.dash ?? null) === JSON.stringify(b.dash ?? null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zerlegt einen Polygon-Ring in zusammenhängende Läufe SICHTBARER Kanten (die
|
||||
* `noStroke`-Kanten werden ausgelassen). Jeder Lauf beginnt an einer sichtbaren
|
||||
* Kante, deren Vorgänger unterdrückt ist. Portiert aus PlanView.visibleEdgeRuns —
|
||||
* so bekommen die inneren Ecken eine Gehrung statt Stumpfkappen.
|
||||
*/
|
||||
function visibleEdgeRuns(pts: RPoint[], noStroke: number[]): RPoint[][] {
|
||||
const n = pts.length;
|
||||
const skip = new Set(noStroke.map((i) => ((i % n) + n) % n));
|
||||
if (skip.size >= n || n < 2) return [];
|
||||
const visible = (i: number) => !skip.has(((i % n) + n) % n);
|
||||
const runs: RPoint[][] = [];
|
||||
for (let s = 0; s < n; s++) {
|
||||
if (!visible(s) || visible(s - 1 + n)) continue; // kein Lauf-Anfang
|
||||
const run: RPoint[] = [pts[s]];
|
||||
let j = s;
|
||||
while (visible(j) && j - s < n) {
|
||||
run.push(pts[(j + 1) % n]);
|
||||
j++;
|
||||
}
|
||||
runs.push(run);
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wandelt einen Plan in eine native render2d-Szene um. Zusammenhängende Umriss-
|
||||
* und Zeichnungskanten werden zu OFFENEN Polylinien verkettet (gehrte Ecken);
|
||||
* nur genuin einzelne Segmente (Türblätter, Referenzlinien) bleiben `lines`.
|
||||
*/
|
||||
export function planToRenderScene(plan: Plan): RScene {
|
||||
const fills: RFill[] = [];
|
||||
const outlines: ROutline[] = [];
|
||||
const polylines: RPolyline[] = [];
|
||||
const lines: RLine[] = [];
|
||||
|
||||
// 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 closed = curPts.length > 2 && samePt(curPts[0], curPts[curPts.length - 1]);
|
||||
if (closed) {
|
||||
outlines.push({ pts: curPts.slice(0, -1), color: col, widthMm: curSrc.weightMm });
|
||||
} else {
|
||||
polylines.push({ pts: curPts, color: col, widthMm: curSrc.weightMm });
|
||||
}
|
||||
}
|
||||
curPts = null;
|
||||
curSrc = null;
|
||||
};
|
||||
|
||||
for (const p of plan.primitives) {
|
||||
if (p.kind === "polygon") {
|
||||
flushRun();
|
||||
const pts: RPoint[] = p.pts.map((v) => [v.x, v.y]);
|
||||
if (pts.length < 2) continue;
|
||||
|
||||
// Füllung
|
||||
const fillCol = toRgba(p.fill, 1);
|
||||
if (fillCol && pts.length >= 3) fills.push({ pts, color: fillCol });
|
||||
|
||||
// Umriss
|
||||
const strokeCol = toRgba(p.stroke, 1);
|
||||
if (strokeCol && p.strokeWidthMm > 0) {
|
||||
const suppressed = p.noStrokeEdges ?? [];
|
||||
if (suppressed.length === 0) {
|
||||
// Ganzer Ring → geschlossener, gehrter Umriss.
|
||||
outlines.push({ pts, color: strokeCol, widthMm: p.strokeWidthMm });
|
||||
} else {
|
||||
// Nur die sichtbaren Kanten, als zusammenhängende (offene) Läufe.
|
||||
for (const run of visibleEdgeRuns(pts, suppressed)) {
|
||||
polylines.push({ pts: run, color: strokeCol, widthMm: p.strokeWidthMm });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (p.kind === "line") {
|
||||
if (p.drawingId) {
|
||||
// Aufeinanderfolgende 2D-Zeichenlinien gleicher drawingId/Stil verketten.
|
||||
const a: RPoint = [p.a.x, p.a.y];
|
||||
const b: RPoint = [p.b.x, p.b.y];
|
||||
if (curPts && curSrc && sameLineStyle(curSrc, p) && samePt(curPts[curPts.length - 1], a)) {
|
||||
curPts.push(b);
|
||||
} else {
|
||||
flushRun();
|
||||
curPts = [a, b];
|
||||
curSrc = p;
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
} else if (p.kind === "arc") {
|
||||
flushRun();
|
||||
const col = toRgba(DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||
// 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 {
|
||||
// "text" wird bewusst übersprungen (render2d rendert keinen Text).
|
||||
flushRun();
|
||||
}
|
||||
}
|
||||
flushRun();
|
||||
|
||||
return { fills, outlines, polylines, lines };
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Leitet aus den Wänden eines Projekts die serialisierbaren WallInput-Records
|
||||
// für den nativen render3d-Renderer ab. Matcht 1:1 render3d::types::WallInput
|
||||
// { start:[x,y], end:[x,y], thickness, height, baseElevation, color:[r,g,b] }
|
||||
// mit Modell-Metern und Y-up-Extrusion (Rust-Seite: plan liegt in XZ, +Y hoch).
|
||||
//
|
||||
// Alle Geschosse werden einbezogen und über wallVerticalExtent korrekt
|
||||
// gestapelt (EG baseElevation 0, OG 2.6 …), sodass das 3D-Fenster das ganze
|
||||
// Gebäude zeigt.
|
||||
|
||||
import type { Project } from "../model/types";
|
||||
import { getWallType, wallTypeThickness } from "../model/types";
|
||||
import { wallVerticalExtent } from "../model/wall";
|
||||
|
||||
export type RVec2 = [number, number];
|
||||
export type RRgb = [number, number, number];
|
||||
|
||||
export interface RWall {
|
||||
start: RVec2;
|
||||
end: RVec2;
|
||||
thickness: number;
|
||||
height: number;
|
||||
baseElevation: number;
|
||||
color: RRgb;
|
||||
}
|
||||
|
||||
/** Warmer, neutraler Wand-Grundton (wie render3d default). */
|
||||
const WALL_RGB: RRgb = [0.82, 0.8, 0.76];
|
||||
|
||||
export function projectToWalls3d(project: Project): RWall[] {
|
||||
const out: RWall[] = [];
|
||||
for (const w of project.walls) {
|
||||
let thickness = 0.2;
|
||||
try {
|
||||
thickness = wallTypeThickness(getWallType(project, w));
|
||||
} catch {
|
||||
thickness = 0.2;
|
||||
}
|
||||
const { zBottom, zTop } = wallVerticalExtent(project, w);
|
||||
const height = Math.max(0.01, zTop - zBottom);
|
||||
out.push({
|
||||
start: [w.start.x, w.start.y],
|
||||
end: [w.end.x, w.end.y],
|
||||
thickness,
|
||||
height,
|
||||
baseElevation: zBottom,
|
||||
color: WALL_RGB,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user