Schnitt/Ansicht: SectionOutput aus render3d an die Zeichenebenen anbinden
- render3d/web.rs: GPU-freies WASM-Binding cut_section_json(model_json, p, n)
→ JSON {cutPolygons, visibleEdges, hiddenEdges} in (u,v)-Metern.
- src/plan/toSection.ts (neu): SectionOutput-Typen, sectionPlaneFromLevel()
leitet die Schnittebene aus linePoints/directionSign ab, computeSection()
flacht über projectToModel3d ab und ruft das Binding.
- src/engine/engine3d.ts (neu): geteilter memoisierter pkg3d-Loader, damit
3D-Viewport und Schnittpfad EINE mod.default()-Init teilen.
- generatePlan.ts: generateSectionPlan() übersetzt SectionOutput in
bestehende Primitive (Cut = polygon mit Poché-Schraffur, sichtbare Kante =
line solid, verdeckte Kante = line dashed) — keine neue Primitive-Art nötig.
- App.tsx: SectionPlanView ersetzt den Stub für Ebenen vom Typ Schnitt/Ansicht;
rendert dasselbe PlanView, mit lokalisierten Hinweisen (lädt/keine Linie/Fehler).
- i18n de/en: section.*-Schlüssel.
- Öffnungen erscheinen korrekt (Brüstung/Sturz-Teilrechtecke) über die
projectToModel3d-Teilkörper-Zerlegung.
Gates: tsc -b 0, build grün (WASM = eigener Lazy-Chunk), render3d cargo test 29/29,
wasm32 --features web check grün.
This commit is contained in:
@@ -66,6 +66,7 @@ import type { Line } from "../model/geometry";
|
||||
export type DetailLevel = "grob" | "mittel" | "fein";
|
||||
import { computeJoins } from "../model/joins";
|
||||
import type { WallCuts } from "../model/joins";
|
||||
import type { SectionOutput } from "./toSection";
|
||||
|
||||
/** Stroke-Farbe der Schicht-Polygone (dunkles Grau). */
|
||||
const POCHE_STROKE = "#2b3039";
|
||||
@@ -401,6 +402,121 @@ export function generatePlan(
|
||||
};
|
||||
}
|
||||
|
||||
// ── Schnitt/Ansicht (abgeleitete Projektion aus render3d::section) ──────────────
|
||||
// Die Rohdaten (Cut-Polygone + sichtbare/verdeckte projizierte Kanten) liefert
|
||||
// der WASM-Extraktor (plan/toSection.ts → section.rs). Hier werden sie 1:1 in
|
||||
// die BESTEHENDEN Plan-Primitive übersetzt — kein neues Primitiv nötig:
|
||||
// • Cut-Polygon → `polygon` (kräftige Poché-Umrisslinie + Schnitt-Schraffur),
|
||||
// • sichtbare Kante → `line` (durchgezogen),
|
||||
// • verdeckte Kante → `line` (gestrichelt).
|
||||
// Damit rendern PlanView (SVG) UND toRenderScene (Engine/WASM-2D) den Schnitt
|
||||
// ohne jede Sonderbehandlung, mit voller Papier-mm-/Parität-Infrastruktur.
|
||||
|
||||
/** Tinte der Schnitt-Poché-Umrisslinie + Schraffur (dunkles Grau). */
|
||||
const SECTION_INK = "#1f242c";
|
||||
/** Tinte der sichtbaren Ansichtskanten. */
|
||||
const SECTION_VISIBLE_INK = "#2b3039";
|
||||
/** Tinte der verdeckten (gestrichelten) Ansichtskanten (etwas heller). */
|
||||
const SECTION_HIDDEN_INK = "#8a929c";
|
||||
/** Strichstärke der Cut-Poché-Umrisslinie (mm Papier, kräftig wie eine Wand). */
|
||||
const SECTION_CUT_OUTLINE_MM = 0.35;
|
||||
/** Strichstärke sichtbarer Ansichtskanten (mm Papier). */
|
||||
const SECTION_VISIBLE_MM = 0.18;
|
||||
/** Strichstärke verdeckter Ansichtskanten (mm Papier, dünner). */
|
||||
const SECTION_HIDDEN_MM = 0.13;
|
||||
/** Strichmuster verdeckter Kanten (mm Papier). */
|
||||
const SECTION_HIDDEN_DASH: number[] = [0.12, 0.08];
|
||||
|
||||
/** Schnitt-Schraffur der Cut-Flächen (diagonale Poché wie ein Wandschnitt). */
|
||||
const SECTION_HATCH: HatchRender = {
|
||||
pattern: "diagonal",
|
||||
scale: 1,
|
||||
angle: 45,
|
||||
color: SECTION_INK,
|
||||
lineWeight: 0.13,
|
||||
dash: null,
|
||||
};
|
||||
|
||||
/** RGB (0..1) → 6-stelliges Hex, für die Cut-Poché-Füllfarbe. */
|
||||
function rgbToHex(rgb: [number, number, number]): string {
|
||||
const c = (v: number) =>
|
||||
Math.max(0, Math.min(255, Math.round(v * 255)))
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
return `#${c(rgb[0])}${c(rgb[1])}${c(rgb[2])}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus einem {@link SectionOutput} (render3d::section, (u, v)-Meter) den
|
||||
* Plan eines Schnitts/einer Ansicht. `u` wird zur Plan-X, `v` (absolute Höhe)
|
||||
* zur Plan-Y — die PlanView zeigt Y nach oben, also erscheint der Schnitt
|
||||
* aufrecht. Rein darstellend; kennt das Projekt nicht.
|
||||
*/
|
||||
export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
|
||||
const primitives: Primitive[] = [];
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
const acc = (x: number, y: number) => {
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
};
|
||||
|
||||
// Cut-Polygone: gefüllte Poché-Fläche (Bauteilfarbe) + Schnitt-Schraffur +
|
||||
// kräftige Umrisslinie — wie eine geschnittene Wand im Grundriss.
|
||||
for (const cp of output.cutPolygons) {
|
||||
if (cp.pts.length < 3) continue;
|
||||
const pts: Vec2[] = cp.pts.map(([u, v]) => {
|
||||
acc(u, v);
|
||||
return { x: u, y: v };
|
||||
});
|
||||
// Mono: reines Weiss statt Bauteilfarbe (nur Umriss + Schraffur tragen).
|
||||
primitives.push({
|
||||
kind: "polygon",
|
||||
pts,
|
||||
fill: mono ? "#ffffff" : rgbToHex(cp.color),
|
||||
stroke: SECTION_INK,
|
||||
strokeWidthMm: SECTION_CUT_OUTLINE_MM,
|
||||
hatch: SECTION_HATCH,
|
||||
});
|
||||
}
|
||||
|
||||
// Verdeckte Kanten zuerst (liegen unter den sichtbaren), gestrichelt.
|
||||
for (const e of output.hiddenEdges) {
|
||||
acc(e.a[0], e.a[1]);
|
||||
acc(e.b[0], e.b[1]);
|
||||
primitives.push({
|
||||
kind: "line",
|
||||
a: { x: e.a[0], y: e.a[1] },
|
||||
b: { x: e.b[0], y: e.b[1] },
|
||||
cls: "section-hidden",
|
||||
weightMm: SECTION_HIDDEN_MM,
|
||||
dash: SECTION_HIDDEN_DASH,
|
||||
color: SECTION_HIDDEN_INK,
|
||||
});
|
||||
}
|
||||
|
||||
// Sichtbare Ansichtskanten, durchgezogen.
|
||||
for (const e of output.visibleEdges) {
|
||||
acc(e.a[0], e.a[1]);
|
||||
acc(e.b[0], e.b[1]);
|
||||
primitives.push({
|
||||
kind: "line",
|
||||
a: { x: e.a[0], y: e.a[1] },
|
||||
b: { x: e.b[0], y: e.b[1] },
|
||||
cls: "section-visible",
|
||||
weightMm: SECTION_VISIBLE_MM,
|
||||
color: SECTION_VISIBLE_INK,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isFinite(minX)) return { primitives, bounds: { minX: 0, minY: 0, maxX: 1, maxY: 1 } };
|
||||
return { primitives, bounds: { minX, minY, maxX, maxY } };
|
||||
}
|
||||
|
||||
/** Dezente Plan-Farbe der Kontext-Konturen (gedämpftes Grau). */
|
||||
const CONTEXT_LINE_COLOR = "#9aa3ad";
|
||||
/** Strichstärke der Kontext-Konturen (mm Papier, dünn). */
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Schnitt-/Ansichts-Pipeline: bindet den analytischen render3d-Schnitt-Extraktor
|
||||
// (`section.rs`, WASM-Export `cut_section_json`) an das Dokumentmodell an.
|
||||
//
|
||||
// Ablauf:
|
||||
// 1. Aus einer Zeichnungsebene (DrawingLevel, kind "section"/"elevation") wird
|
||||
// eine Schnittebene abgeleitet — die Grundriss-Schnittlinie `linePoints`
|
||||
// plus `directionSign` (Blickrichtung).
|
||||
// 2. Das Projekt wird über `projectToModel3d` (dieselbe Wand-/Decken-Zerlegung
|
||||
// wie der 3D-Viewport) zu Wänden + Decken geflacht und als JSON an
|
||||
// `cut_section_json` gereicht.
|
||||
// 3. Das zurückgegebene `SectionOutput` (Cut-Polygone + sichtbare/verdeckte
|
||||
// Kanten, alles in (u, v)-Metern) wird in `generateSectionPlan`
|
||||
// (generatePlan.ts) zu Plan-Primitiven übersetzt.
|
||||
//
|
||||
// Einheiten: METER. Koordinaten der Ausgabe: u = horizontal entlang der Ebene,
|
||||
// v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs.
|
||||
|
||||
import type { DrawingLevel, Project } from "../model/types";
|
||||
import { projectToModel3d } from "./toWalls3d";
|
||||
import { loadEngine3d } from "../engine/engine3d";
|
||||
|
||||
/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */
|
||||
export interface SectionComponentRef {
|
||||
kind: "wall" | "slab";
|
||||
index: number;
|
||||
}
|
||||
|
||||
/** Ein geschnittenes Bauteil-Rechteck in (u, v)-Metern (Ring, implizit geschlossen). */
|
||||
export interface SectionCutPolygon {
|
||||
component: SectionComponentRef;
|
||||
/** Albedo-Farbe des Bauteils (RGB 0..1). */
|
||||
color: [number, number, number];
|
||||
/** Ring-Ecken (u, v). */
|
||||
pts: Array<[number, number]>;
|
||||
}
|
||||
|
||||
/** Ein projiziertes Liniensegment in (u, v)-Metern. */
|
||||
export interface SectionEdge {
|
||||
component: SectionComponentRef;
|
||||
a: [number, number];
|
||||
b: [number, number];
|
||||
}
|
||||
|
||||
/** Vollständige Ausgabe eines Schnitt-/Ansichts-Laufs (JSON von `cut_section_json`). */
|
||||
export interface SectionOutput {
|
||||
cutPolygons: SectionCutPolygon[];
|
||||
visibleEdges: SectionEdge[];
|
||||
hiddenEdges: SectionEdge[];
|
||||
}
|
||||
|
||||
/** Minimal-Typ des WASM-Exports (das echte `.d.ts` entsteht erst beim wasm-Build). */
|
||||
interface Engine3dSection {
|
||||
cut_section_json(
|
||||
modelJson: string,
|
||||
px: number,
|
||||
py: number,
|
||||
pz: number,
|
||||
nx: number,
|
||||
ny: number,
|
||||
nz: number,
|
||||
): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Schnittebene in world-Koordinaten (Y-up): Punkt auf der Ebene +
|
||||
* Normale (= Blickrichtung, zeigt vom Betrachter ins Modell).
|
||||
*/
|
||||
export interface SectionPlaneSpec {
|
||||
point: [number, number, number];
|
||||
normal: [number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet aus einer Schnitt-/Ansichtsebene die world-Schnittebene ab. Die
|
||||
* Grundriss-Schnittlinie `linePoints` [p0, p1] (Modell-Meter) liegt in der
|
||||
* XZ-Ebene (world.x = model.x, world.z = model.y); die Ebenennormale steht
|
||||
* senkrecht auf der Linie in der Horizontalen und wird über `directionSign`
|
||||
* (Voreinstellung +1) in die gewünschte Blickrichtung gedreht.
|
||||
*
|
||||
* Gibt `null` zurück, wenn die Ebene (noch) keine Schnittlinie trägt (z. B. eine
|
||||
* frisch angelegte Ansicht ohne gesetzte Linie) — der Aufrufer zeigt dann einen
|
||||
* Hinweis statt eines leeren Plans.
|
||||
*/
|
||||
export function sectionPlaneFromLevel(level: DrawingLevel): SectionPlaneSpec | null {
|
||||
const line = level.linePoints;
|
||||
if (!line) return null;
|
||||
const [p0, p1] = line;
|
||||
const dx = p1.x - p0.x;
|
||||
const dy = p1.y - p0.y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
if (len < 1e-9) return null;
|
||||
const ux = dx / len;
|
||||
const uy = dy / len;
|
||||
// Links-Normale der Linie im Grundriss (n = (-u.y, u.x)), per directionSign
|
||||
// in die Blickrichtung gedreht. Modell (nx, ny) → world (nx, 0, ny).
|
||||
const sign = level.directionSign ?? 1;
|
||||
const nx = -uy * sign;
|
||||
const ny = ux * sign;
|
||||
return {
|
||||
point: [p0.x, 0, p0.y],
|
||||
normal: [nx, 0, ny],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet den Schnitt/die Ansicht der Ebene `level` gegen das Modell `project`
|
||||
* über den WASM-Extraktor. Asynchron (das render3d-WASM-Modul wird lazy geladen).
|
||||
*
|
||||
* Gibt `null` zurück, wenn die Ebene keine gültige Schnittlinie hat. Wirft, wenn
|
||||
* das WASM-Modul den Export (noch) nicht bereitstellt — der Aufrufer fängt das ab
|
||||
* und blendet einen Fallback ein (pkg3d muss dann über `npm run build:engine3d`
|
||||
* neu gebaut werden, damit `cut_section_json` verfügbar ist).
|
||||
*/
|
||||
export async function computeSection(
|
||||
project: Project,
|
||||
level: DrawingLevel,
|
||||
): Promise<SectionOutput | null> {
|
||||
const plane = sectionPlaneFromLevel(level);
|
||||
if (!plane) return null;
|
||||
const mod = (await loadEngine3d()) as Engine3dSection;
|
||||
if (typeof mod.cut_section_json !== "function") {
|
||||
throw new Error(
|
||||
"render3d-WASM ohne cut_section_json — pkg3d neu bauen (npm run build:engine3d)",
|
||||
);
|
||||
}
|
||||
const modelJson = JSON.stringify(projectToModel3d(project));
|
||||
const json = mod.cut_section_json(
|
||||
modelJson,
|
||||
plane.point[0],
|
||||
plane.point[1],
|
||||
plane.point[2],
|
||||
plane.normal[0],
|
||||
plane.normal[1],
|
||||
plane.normal[2],
|
||||
);
|
||||
return JSON.parse(json) as SectionOutput;
|
||||
}
|
||||
Reference in New Issue
Block a user