/** 3D-Rendering der Scene-Dressing-Ebene (rein visuelle Deko, getrennt vom Plan). * * Konzept: FP_Kopf/vault/50_Umsetzung/Scene-Dressing-Konzept.md * * Zwei Bausteine: * - **Prozedurale Kits** (`DRESSING_BAUSAETZE`) im gleichen Box/Zylinder/Kugel- * Muster wie {@link ./moebel3d.tsx} – Ursprung = bbox-Mitte, jedes Bauteil * bleibt garantiert in der bbox (clampTeil). So braucht der Deko-Durchstich * weder Assets noch Speicher. * - **Auto-Upgrade auf echte glTF-Modelle:** trägt ein Deko-Objekt * `assetStatus:"modeled"` + `gltfRef`, lädt {@link DressingObjekt3D} das Binary * aus `/assets/dressing/.glb` (Ursprung Mitte-unten) und ersetzt den * Platzhalter – ohne Codeänderung. Upload-Konvention: * `apps/web/public/assets/dressing/README.md`. * * Deko ist **nicht auswählbar und nicht norm-relevant**: die Meshes haben kein * Klick-Handling und deaktivieren das Raycasting, damit Auswahl/Ampel/Solver- * Logik im Viewer völlig unberührt bleiben. */ import { RoundedBox, useGLTF } from "@react-three/drei"; import { Suspense, useMemo } from "react"; import { Vector2 } from "three"; import type { DressingItem, KatalogItem, Plan, Room } from "./api"; import { sceneDressing, type DressingPlatzierung } from "./dressing"; import { DRESSING_LICHT_TYPEN, dressingBauteile } from "./dressing3d-kits"; import { rolleFarbe, type Rolle, type Teil } from "./moebel3d.tsx"; // raycast=noop: Klicks gehen «durch» die Deko → Möbelauswahl/Deselektion bleiben. const noRaycast = () => null; /** Warmweisser Leuchtton des Diffusors (Licht-Kits) – bewusst fix, nicht * stilabhängig, damit die Leuchte in jeder Stilvariante als «an» erkennbar * bleibt (siehe DRESSING_LICHT_TYPEN in dressing3d-kits.ts). */ const DIFFUSOR_FARBE = "#fff2d6"; const DIFFUSOR_EMISSIVE = "#ffb763"; /** Oberflächen-Physik je Rolle – spiegelt {@link ./moebel3d.tsx} (glas transparent, * chrom poliert), damit Deko und Möbel denselben Material-Look tragen. * `leuchtet` (additiv, nur für Licht-Kits): der Diffusor-Bauteil bekommt statt * der normalen Materialrolle eine warmweisse Emissiv-Fläche. */ function DressingMaterial({ rolle, farbwert, leuchtet, }: { rolle: Rolle; farbwert: string; leuchtet?: boolean; }) { if (leuchtet) { return ( ); } if (rolle === "glas") { return ( ); } if (rolle === "chrom") { // Wie moebel3d: mit Environment-Map echtes poliertes Chrom (spiegelt Umgebung). return ( ); } const roughness = rolle === "dunkel" ? 0.62 : rolle === "hell" ? 0.5 : 0.55; return ( ); } /** Ein Bauteil als Mesh – Raycasting deaktiviert (Deko blockiert nie Auswahl). * Deckt dieselben Primitive wie {@link ./moebel3d.tsx} ab (box/rundbox/zylinder/ * kugel/lathe/torus), damit die volumetrischen Deko-Kits vollständig rendern. */ function DressingTeilMesh({ teil, farbe, leuchtet, }: { teil: Teil; farbe: string; leuchtet?: boolean; }) { const farbwert = rolleFarbe(teil.rolle, farbe); const material = ; if (teil.form === "box") { return ( {material} ); } if (teil.form === "rundbox") { const minHalb = Math.min(...teil.groesse) / 2; const radius = Math.max(0, Math.min(teil.radius, minHalb - 1e-4)); if (radius < 1e-4) { return ( {material} ); } return ( {material} ); } if (teil.form === "zylinder") { return ( {material} ); } if (teil.form === "kugel") { return ( {material} ); } if (teil.form === "lathe") { const punkte = teil.profil.map(([r, y]) => new Vector2(Math.max(0, r), y)); return ( {material} ); } const rot: [number, number, number] = teil.achse === "y" ? [Math.PI / 2, 0, 0] : teil.achse === "x" ? [0, Math.PI / 2, 0] : [0, 0, 0]; return ( {material} ); } /** Prozedurale Deko (Platzhalter): Primitiv-Kit in der bbox der Deko-Masse. * Additiv für Licht-Typen (DRESSING_LICHT_TYPEN, z.B. pendelleuchte/ * tischleuchte): der Diffusor-Bauteil (rolle "hell") leuchtet emissiv und * bekommt eine kleine, warme Punktlichtquelle an seiner Position – die * restliche Kit-/Material-Logik bleibt unverändert. */ function DressingProzedural({ platz }: { platz: DressingPlatzierung }) { const { w, d, h } = platz.masse; const teile = dressingBauteile(platz.funktionsTyp, w, d, h); const istLicht = DRESSING_LICHT_TYPEN.has(platz.funktionsTyp); const diffusor = istLicht ? teile.find((t) => t.rolle === "hell") : undefined; return ( <> {teile.map((teil, i) => ( ))} {diffusor && ( )} ); } /** Echtes glTF-Modell (Auto-Upgrade). Ursprung Mitte-unten → um −h/2 versetzt, * damit es wie das Primitiv-Kit um die bbox-Mitte platziert werden kann. */ function DressingGltf({ gltfRef, h }: { gltfRef: string; h: number }) { const { scene } = useGLTF(`/assets/dressing/${gltfRef}.glb`); const klon = useMemo(() => scene.clone(true), [scene]); return ( ); } /** Ein Deko-Objekt an seiner Welt-Pose: echtes Modell falls vorhanden, sonst * prozeduraler Platzhalter. Die Gruppe trägt Position (bbox-Mitte) und Yaw. */ function DressingObjekt3D({ platz }: { platz: DressingPlatzierung }) { const modelliert = platz.assetStatus === "modeled" && !!platz.gltfRef; return ( {modelliert && platz.gltfRef ? ( }> ) : ( )} ); } /** * Scene-Dressing-Ebene für den 3D-Viewer. Rechnet die Deko-Platzierungen * deterministisch aus (room, plan, catalog, styleProfile, seed) und rendert sie. * Der Plan wird nur GELESEN – die Ebene fliesst nie in placements/Report/LV. * `sichtbar=false` blendet sie komplett aus (Toggle «Deko»). */ export function DressingLayer3D({ room, plan, catalog, styleProfile, dressingItems, sichtbar, }: { room: Room; plan: Plan; catalog: KatalogItem[]; styleProfile: { styleVector: Record } | null; dressingItems: DressingItem[]; sichtbar: boolean; }) { const platzierungen = useMemo( () => sceneDressing(room, plan, catalog, styleProfile, dressingItems, plan.meta.seed), [room, plan, catalog, styleProfile, dressingItems], ); if (!sichtbar) return null; return ( <> {platzierungen.map((platz) => ( ))} ); }