Spaces:
Running
Running
Download apps/web/src/dressing3d.tsx from Bryan-HSLU/FP_POC: direct link, hf CLI and curl.
- Browser
- Download file 8.76 kB
-
https://huggingface.co/spaces/Bryan-HSLU/FP_POC/resolve/main/apps/web/src/dressing3d.tsx
- Command line
-
hf download hf://spaces/Bryan-HSLU/FP_POC/apps/web/src/dressing3d.tsx
-
curl -L -o dressing3d.tsx https://huggingface.co/spaces/Bryan-HSLU/FP_POC/resolve/main/apps/web/src/dressing3d.tsx
8.76 kB
| /** 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/<gltfRef>.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 ( | |
| <meshStandardMaterial | |
| color={DIFFUSOR_FARBE} | |
| emissive={DIFFUSOR_EMISSIVE} | |
| emissiveIntensity={1.1} | |
| roughness={0.4} | |
| metalness={0} | |
| /> | |
| ); | |
| } | |
| if (rolle === "glas") { | |
| return ( | |
| <meshStandardMaterial | |
| color={farbwert} | |
| transparent | |
| opacity={0.32} | |
| roughness={0.05} | |
| metalness={0} | |
| envMapIntensity={1.0} | |
| /> | |
| ); | |
| } | |
| if (rolle === "chrom") { | |
| // Wie moebel3d: mit Environment-Map echtes poliertes Chrom (spiegelt Umgebung). | |
| return ( | |
| <meshStandardMaterial | |
| color={farbwert} | |
| metalness={1.0} | |
| roughness={0.08} | |
| envMapIntensity={1.1} | |
| /> | |
| ); | |
| } | |
| const roughness = rolle === "dunkel" ? 0.62 : rolle === "hell" ? 0.5 : 0.55; | |
| return ( | |
| <meshStandardMaterial | |
| color={farbwert} | |
| metalness={0.03} | |
| roughness={roughness} | |
| envMapIntensity={0.45} | |
| /> | |
| ); | |
| } | |
| /** 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 = <DressingMaterial rolle={teil.rolle} farbwert={farbwert} leuchtet={leuchtet} />; | |
| if (teil.form === "box") { | |
| return ( | |
| <mesh position={teil.pos} raycast={noRaycast}> | |
| <boxGeometry args={teil.groesse} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| 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 ( | |
| <mesh position={teil.pos} raycast={noRaycast}> | |
| <boxGeometry args={teil.groesse} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| return ( | |
| <RoundedBox | |
| position={teil.pos} | |
| args={teil.groesse} | |
| radius={radius} | |
| smoothness={3} | |
| steps={1} | |
| raycast={noRaycast} | |
| > | |
| {material} | |
| </RoundedBox> | |
| ); | |
| } | |
| if (teil.form === "zylinder") { | |
| return ( | |
| <mesh position={teil.pos} raycast={noRaycast}> | |
| <cylinderGeometry args={[teil.rTop, teil.rBottom, teil.hoehe, 16]} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| if (teil.form === "kugel") { | |
| return ( | |
| <mesh position={teil.pos} raycast={noRaycast}> | |
| <sphereGeometry args={[teil.radius, 14, 10]} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| if (teil.form === "lathe") { | |
| const punkte = teil.profil.map(([r, y]) => new Vector2(Math.max(0, r), y)); | |
| return ( | |
| <mesh position={teil.pos} raycast={noRaycast}> | |
| <latheGeometry args={[punkte, teil.segmente]} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| 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 ( | |
| <mesh position={teil.pos} rotation={rot} raycast={noRaycast}> | |
| <torusGeometry args={[teil.radius, teil.roehre, 16, 32]} /> | |
| {material} | |
| </mesh> | |
| ); | |
| } | |
| /** 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) => ( | |
| <DressingTeilMesh | |
| key={i} | |
| teil={teil} | |
| farbe={platz.farbe} | |
| leuchtet={istLicht && teil.rolle === "hell"} | |
| /> | |
| ))} | |
| {diffusor && ( | |
| <pointLight | |
| position={diffusor.pos} | |
| color="#ffcf94" | |
| intensity={0.5} | |
| distance={3.5} | |
| decay={2} | |
| /> | |
| )} | |
| </> | |
| ); | |
| } | |
| /** 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 ( | |
| <group position={[0, -h / 2, 0]}> | |
| <primitive object={klon} /> | |
| </group> | |
| ); | |
| } | |
| /** 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 ( | |
| <group position={platz.pos} rotation={[0, (-platz.yawDeg * Math.PI) / 180, 0]}> | |
| {modelliert && platz.gltfRef ? ( | |
| <Suspense fallback={<DressingProzedural platz={platz} />}> | |
| <DressingGltf gltfRef={platz.gltfRef} h={platz.masse.h} /> | |
| </Suspense> | |
| ) : ( | |
| <DressingProzedural platz={platz} /> | |
| )} | |
| </group> | |
| ); | |
| } | |
| /** | |
| * 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<string, number> } | 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) => ( | |
| <DressingObjekt3D key={platz.id} platz={platz} /> | |
| ))} | |
| </> | |
| ); | |
| } | |