3D: Bauteil-/Materialfarbe statt Einheitsgrau für Wände & Decken
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.
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { sampleProject } from "../model/sampleProject";
|
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 RGB: [number, number, number] = [1, 0.5, 0.1];
|
||||||
const FLOATS_PER_VERTEX = 6; // [px,py,pz, r,g,b]
|
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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+49
-12
@@ -26,8 +26,14 @@
|
|||||||
// render3d selbständig aus der Wandkonnektivität, braucht also keine
|
// render3d selbständig aus der Wandkonnektivität, braucht also keine
|
||||||
// zusätzlichen Felder von hier.
|
// zusätzlichen Felder von hier.
|
||||||
|
|
||||||
import type { Project, Wall } from "../model/types";
|
import type { Project, Wall, Layer } from "../model/types";
|
||||||
import { getComponent, getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
import {
|
||||||
|
getComponent,
|
||||||
|
getWallType,
|
||||||
|
getCeilingType,
|
||||||
|
wallTypeThickness,
|
||||||
|
openingsOfWall,
|
||||||
|
} from "../model/types";
|
||||||
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
||||||
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
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
|
* (0..1, render3d-Konvention) um. Ungültiges/fehlendes Format fällt auf den
|
||||||
* neutralen Wand-Grundton zurück (nie ein hartes Fehlbild).
|
* neutralen Wand-Grundton zurück (nie ein hartes Fehlbild).
|
||||||
*/
|
*/
|
||||||
function hexToRgb(hex: string): RRgb {
|
function hexToRgb(hex: string, fallback: RRgb = WALL_RGB): RRgb {
|
||||||
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex ?? "");
|
||||||
if (!m) return WALL_RGB;
|
if (!m) return fallback;
|
||||||
const v = parseInt(m[1], 16);
|
const v = parseInt(m[1], 16);
|
||||||
return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255];
|
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
|
* Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab
|
||||||
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
||||||
@@ -138,6 +161,7 @@ function pushSegment(
|
|||||||
thickness: number,
|
thickness: number,
|
||||||
layers: RLayer[],
|
layers: RLayer[],
|
||||||
wallId: string,
|
wallId: string,
|
||||||
|
color: RRgb,
|
||||||
): void {
|
): void {
|
||||||
if (to - from <= EPS) return;
|
if (to - from <= EPS) return;
|
||||||
if (zTop - zBottom <= EPS) return;
|
if (zTop - zBottom <= EPS) return;
|
||||||
@@ -155,7 +179,7 @@ function pushSegment(
|
|||||||
thickness,
|
thickness,
|
||||||
height: zTop - zBottom,
|
height: zTop - zBottom,
|
||||||
baseElevation: zBottom,
|
baseElevation: zBottom,
|
||||||
color: WALL_RGB,
|
color,
|
||||||
layers,
|
layers,
|
||||||
wallId,
|
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
|
// leer, wenn kein WallType auflösbar ist (render3d extrudiert dann wie
|
||||||
// bisher einen Vollkörper aus `thickness`/`color`, siehe RWall-Moduldoc).
|
// bisher einen Vollkörper aus `thickness`/`color`, siehe RWall-Moduldoc).
|
||||||
let layers: RLayer[] = [];
|
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 {
|
try {
|
||||||
const wt = getWallType(project, wall);
|
const wt = getWallType(project, wall);
|
||||||
thickness = wallTypeThickness(wt);
|
thickness = wallTypeThickness(wt);
|
||||||
@@ -175,9 +202,11 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
|||||||
thickness: l.thickness,
|
thickness: l.thickness,
|
||||||
color: hexToRgb(getComponent(project, l.componentId).color),
|
color: hexToRgb(getComponent(project, l.componentId).color),
|
||||||
}));
|
}));
|
||||||
|
color = dominantLayerColor(wt.layers, project, WALL_RGB);
|
||||||
} catch {
|
} catch {
|
||||||
thickness = 0.2;
|
thickness = 0.2;
|
||||||
layers = [];
|
layers = [];
|
||||||
|
color = WALL_RGB;
|
||||||
}
|
}
|
||||||
const { zBottom, zTop } = wallVerticalExtent(project, wall);
|
const { zBottom, zTop } = wallVerticalExtent(project, wall);
|
||||||
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
|
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).
|
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
|
||||||
if (cutouts.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,23 +248,23 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
|||||||
const segFrom = Math.max(cursor, from);
|
const segFrom = Math.max(cursor, from);
|
||||||
if (from > cursor) {
|
if (from > cursor) {
|
||||||
// Voller Wandpfeiler bis zur Aussparung.
|
// 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) {
|
if (to > segFrom) {
|
||||||
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
|
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
|
||||||
if (oBottom > zBottom + EPS) {
|
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).
|
// Sturz über der Öffnung (bis zum Wandkopf).
|
||||||
if (oTop < zTop - EPS) {
|
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);
|
cursor = Math.max(cursor, to);
|
||||||
}
|
}
|
||||||
// Restlicher Wandpfeiler bis zum Achsenende.
|
// Restlicher Wandpfeiler bis zum Achsenende.
|
||||||
if (cursor < axisLen) {
|
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).
|
// bereits über ceilingThickness (respektiert Typ-Dicke + Übersteuerung).
|
||||||
const { zBottom, zTop } = ceilingVerticalExtent(project, c);
|
const { zBottom, zTop } = ceilingVerticalExtent(project, c);
|
||||||
if (zTop - zBottom <= EPS) continue;
|
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({
|
out.push({
|
||||||
outline: c.outline.map((p) => [p.x, p.y] as RVec2),
|
outline: c.outline.map((p) => [p.x, p.y] as RVec2),
|
||||||
zBottom,
|
zBottom,
|
||||||
zTop,
|
zTop,
|
||||||
color: SLAB_RGB,
|
color,
|
||||||
ceilingId: c.id,
|
ceilingId: c.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user