Merge: Ansicht (Elevation) als echte 2D-Darstellung + Schatten-Toggle

toElevation.ts: Painter-Projektion des 3D-Modells auf die Ansichtsebene
(Fassaden-/Deckenflächen fern→nah, Rückseiten-Culling, Fenster/Türen als
Rahmen+Glas, Bodenlinie) + klassischer 45°-Schlagschatten auskragender
Bauteile (Sutherland–Hodgman auf die Empfängerfläche geclippt). Schatten-
Umschalter in der Ansichts-Leiste (DrawingLevel.shadows), App rendert
elevation-Ebenen über den neuen Generator (synchron, ohne WASM).
This commit is contained in:
2026-07-11 00:26:02 +02:00
7 changed files with 1007 additions and 7 deletions
+32 -7
View File
@@ -51,6 +51,7 @@ import { generatePlan, generateSectionPlan } from "./plan/generatePlan";
import type { CategoryDisplayResolver } from "./plan/generatePlan";
import { computeSection } from "./plan/toSection";
import type { SectionOutput } from "./plan/toSection";
import { generateElevationPlan } from "./plan/toElevation";
import { pushNativeScene, pushNativeWalls } from "./plan/nativeSync";
import { selectionHighlightLines } from "./plan/toWalls3d";
import { roofBaseElevation, roofGeometry } from "./geometry/roof";
@@ -4786,6 +4787,16 @@ export default function App() {
renderModeEnabled={viewType === "perspektive"}
planColorMode={planColorMode}
onPlanColorMode={setPlanColorMode}
shadowsAvailable={activeLevel.kind === "elevation"}
shadows={activeLevel.shadows ?? false}
onShadows={(v) =>
setProject((p) => ({
...p,
drawingLevels: p.drawingLevels.map((z) =>
z.id === activeLevel.id ? { ...z, shadows: v } : z,
),
}))
}
combos={{
// Ebenen-Kombinationen: Speichern = Snapshot des Layer-Baums →
// localStorage; Laden = Map lesen → Sichtbarkeits-Flags anwenden.
@@ -6672,12 +6683,17 @@ function SectionPlanView({
selectedDrawingIds?: string[];
gripHandlers: GripHandlers;
}) {
// Die Ansicht (kind "elevation") läuft NICHT über den WASM-Schnitt, sondern
// über den rein TS-seitigen Flächen-Generator (Painter, toElevation.ts) —
// synchron, kein Lazy-Load. Nur der echte Schnitt braucht die WASM-Pipeline.
const isElevation = level.kind === "elevation";
const [output, setOutput] = useState<SectionOutput | null>(null);
const [status, setStatus] = useState<"loading" | "ready" | "empty" | "error">(
"loading",
);
useEffect(() => {
if (isElevation) return; // Ansicht: kein WASM-Schnitt (synchroner Generator).
let disposed = false;
setStatus("loading");
setOutput(null);
@@ -6699,20 +6715,29 @@ function SectionPlanView({
return () => {
disposed = true;
};
}, [project, level]);
}, [project, level, isElevation]);
const plan = useMemo(
() => (output ? generateSectionPlan(output, mono) : null),
[output, mono],
// Ansichts-Plan: gefüllte Fassadenansicht inkl. optionalem Schatten
// (level.shadows). Liefert null, wenn die Ebene keine Ansichtslinie trägt.
const elevationPlan = useMemo(
() => (isElevation ? generateElevationPlan(project, level, { mono }) : null),
[isElevation, project, level, mono],
);
const sectionPlan = useMemo(
() => (!isElevation && output ? generateSectionPlan(output, mono) : null),
[isElevation, output, mono],
);
const plan = isElevation ? elevationPlan : sectionPlan;
const effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status;
const kindLabel = level.kind === "section" ? t("stub.section") : t("stub.elevation");
if (!plan || status !== "ready") {
if (!plan || effStatus !== "ready") {
const note =
status === "empty"
effStatus === "empty"
? t("section.empty", { kind: kindLabel })
: status === "error"
: effStatus === "error"
? t("section.error", { kind: kindLabel })
: t("section.loading", { kind: kindLabel });
return (