From 6ea562cedab7635b41f9f5ca3ff0ede42aaaf713 Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 4 Jul 2026 06:31:48 +0200 Subject: [PATCH] =?UTF-8?q?3D:=20Bauteil-/Materialfarbe=20statt=20Einheits?= =?UTF-8?q?grau=20f=C3=BCr=20W=C3=A4nde=20&=20Decken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit projectToModel3d trug bisher für alle Wände WALL_RGB und alle Decken SLAB_RGB (konstant grau), obwohl die Component-Materialfarbe verfügbar ist. Jetzt: repräsentative Farbe der dicksten Schicht (tragende/ dominante Lage) je Wandtyp bzw. Deckentyp, Fallback auf die Konstanten. Reine Albedo-Änderung — Geometrie, Picking und Highlight unberührt. Schicht-Teilquader je Materiallage (echter 3D-Wandaufbau) bleibt als Folgeschritt vermerkt. --- src/plan/toWalls3d.test.ts | 28 ++++++++++++++++- src/plan/toWalls3d.ts | 61 ++++++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/plan/toWalls3d.test.ts b/src/plan/toWalls3d.test.ts index 9d851f1..6fcf5ec 100644 --- a/src/plan/toWalls3d.test.ts +++ b/src/plan/toWalls3d.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect } from "vitest"; import { sampleProject } from "../model/sampleProject"; -import { selectionHighlightLines } from "./toWalls3d"; +import { selectionHighlightLines, projectToWalls3d, projectToModel3d } from "./toWalls3d"; const RGB: [number, number, number] = [1, 0.5, 0.1]; const FLOATS_PER_VERTEX = 6; // [px,py,pz, r,g,b] @@ -68,3 +68,29 @@ describe("selectionHighlightLines", () => { expect(lines.length % (12 * VERTICES_PER_EDGE * FLOATS_PER_VERTEX)).toBe(0); }); }); + +describe("Bauteil-/Materialfarbe statt Einheits-Grau (Wände + Decken)", () => { + it("Wand -> Farbe der dicksten Schicht (aw: insulation 0.16 m, #ffffff)", () => { + // Wandtyp "aw" (s. sampleProject): render-ext 0.02, insulation 0.16, + // brick 0.15, render-int 0.015 -> dickste Schicht = insulation (#ffffff). + const walls = projectToWalls3d(sampleProject); + const w2 = walls.filter((w) => w.wallId === "W2"); + expect(w2.length).toBeGreaterThan(0); + for (const seg of w2) { + expect(seg.color[0]).toBeCloseTo(1, 6); + expect(seg.color[1]).toBeCloseTo(1, 6); + expect(seg.color[2]).toBeCloseTo(1, 6); + } + }); + + it("Decke -> Farbe der dicksten Schicht (dg-massiv: concrete 0.2 m, #9aa0a6)", () => { + // Deckentyp "dg-massiv": screed 0.06, insulation 0.04, concrete 0.2 -> + // dickste Schicht = concrete (#9aa0a6). + const { slabs } = projectToModel3d(sampleProject); + const c1 = slabs.find((s) => s.ceilingId === "C1"); + expect(c1).toBeDefined(); + expect(c1!.color[0]).toBeCloseTo(0x9a / 255, 6); + expect(c1!.color[1]).toBeCloseTo(0xa0 / 255, 6); + expect(c1!.color[2]).toBeCloseTo(0xa6 / 255, 6); + }); +}); diff --git a/src/plan/toWalls3d.ts b/src/plan/toWalls3d.ts index 291fb80..cf3dd8c 100644 --- a/src/plan/toWalls3d.ts +++ b/src/plan/toWalls3d.ts @@ -26,8 +26,14 @@ // render3d selbständig aus der Wandkonnektivität, braucht also keine // zusätzlichen Felder von hier. -import type { Project, Wall } from "../model/types"; -import { getComponent, getWallType, wallTypeThickness, openingsOfWall } from "../model/types"; +import type { Project, Wall, Layer } from "../model/types"; +import { + getComponent, + getWallType, + getCeilingType, + wallTypeThickness, + openingsOfWall, +} from "../model/types"; import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall"; import { openingInterval, openingVerticalExtent } from "../geometry/opening"; @@ -115,13 +121,30 @@ const EPS = 1e-4; * (0..1, render3d-Konvention) um. Ungültiges/fehlendes Format fällt auf den * neutralen Wand-Grundton zurück (nie ein hartes Fehlbild). */ -function hexToRgb(hex: string): RRgb { - const m = /^#?([0-9a-fA-F]{6})$/.exec(hex); - if (!m) return WALL_RGB; +function hexToRgb(hex: string, fallback: RRgb = WALL_RGB): RRgb { + const m = /^#?([0-9a-fA-F]{6})$/.exec(hex ?? ""); + if (!m) return fallback; const v = parseInt(m[1], 16); return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255]; } +/** + * Repräsentative Bauteilfarbe eines Schichtaufbaus: die Farbe der DICKSTEN + * Schicht (i. d. R. die tragende/dominante Schicht, z. B. Mauerwerk/Beton) — + * einfache Näherung, solange Wände/Decken als EIN Vollkörper emittiert werden + * (kein Schicht-Rendering in 3D). Fällt auf `fallback` zurück, wenn keine + * Schicht vorhanden ist oder deren Component nicht auflösbar ist. + */ +function dominantLayerColor(layers: Layer[], project: Project, fallback: RRgb): RRgb { + if (layers.length === 0) return fallback; + const dominant = layers.reduce((best, l) => (l.thickness > best.thickness ? l : best)); + try { + return hexToRgb(getComponent(project, dominant.componentId).color, fallback); + } catch { + return fallback; + } +} + /** * Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab * Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke @@ -138,6 +161,7 @@ function pushSegment( thickness: number, layers: RLayer[], wallId: string, + color: RRgb, ): void { if (to - from <= EPS) return; if (zTop - zBottom <= EPS) return; @@ -155,7 +179,7 @@ function pushSegment( thickness, height: zTop - zBottom, baseElevation: zBottom, - color: WALL_RGB, + color, layers, wallId, }); @@ -168,6 +192,9 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void { // leer, wenn kein WallType auflösbar ist (render3d extrudiert dann wie // bisher einen Vollkörper aus `thickness`/`color`, siehe RWall-Moduldoc). let layers: RLayer[] = []; + // Repräsentative Vollkörper-Farbe = Farbe der dicksten Schicht (Option A, + // siehe Moduldoc); Fallback WALL_RGB, wenn kein WallType auflösbar ist. + let color: RRgb = WALL_RGB; try { const wt = getWallType(project, wall); thickness = wallTypeThickness(wt); @@ -175,9 +202,11 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void { thickness: l.thickness, color: hexToRgb(getComponent(project, l.componentId).color), })); + color = dominantLayerColor(wt.layers, project, WALL_RGB); } catch { thickness = 0.2; layers = []; + color = WALL_RGB; } const { zBottom, zTop } = wallVerticalExtent(project, wall); const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y); @@ -207,7 +236,7 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void { // Ohne Aussparungen: ein durchgehender Quader (wie bisher). if (cutouts.length === 0) { - pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers, wall.id); + pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers, wall.id, color); return; } @@ -219,23 +248,23 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void { const segFrom = Math.max(cursor, from); if (from > cursor) { // Voller Wandpfeiler bis zur Aussparung. - pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers, wall.id); + pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers, wall.id, color); } if (to > segFrom) { // Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom). if (oBottom > zBottom + EPS) { - pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers, wall.id); + pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers, wall.id, color); } // Sturz über der Öffnung (bis zum Wandkopf). if (oTop < zTop - EPS) { - pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers, wall.id); + pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers, wall.id, color); } } cursor = Math.max(cursor, to); } // Restlicher Wandpfeiler bis zum Achsenende. if (cursor < axisLen) { - pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers, wall.id); + pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers, wall.id, color); } } @@ -248,11 +277,19 @@ function emitSlabs(project: Project): RSlab[] { // bereits über ceilingThickness (respektiert Typ-Dicke + Übersteuerung). const { zBottom, zTop } = ceilingVerticalExtent(project, c); if (zTop - zBottom <= EPS) continue; + // Repräsentative Farbe = dickste Schicht des Deckentyps (analog Wände); + // kein auflösbarer Deckentyp ⇒ SLAB_RGB-Fallback. + let color: RRgb = SLAB_RGB; + try { + color = dominantLayerColor(getCeilingType(project, c).layers, project, SLAB_RGB); + } catch { + color = SLAB_RGB; + } out.push({ outline: c.outline.map((p) => [p.x, p.y] as RVec2), zBottom, zTop, - color: SLAB_RGB, + color, ceilingId: c.id, }); }