3D: Glasscheiben in Fenstern (AUDIT A5-Teilschritt)
Jede Fensteröffnung bekommt im 3D eine dünne, leicht bläuliche Glasscheibe (RMesh, mittig in der Wanddicke, UK=baseElevation+sillHeight bis OK), damit Fenster als Fenster lesbar sind statt als Löcher. Türen bleiben offen. Nur meshes-Ebene → pickGeometry/Highlight unberührt.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
|
import type { Project } from "../model/types";
|
||||||
import { sampleProject } from "../model/sampleProject";
|
import { sampleProject } from "../model/sampleProject";
|
||||||
import { selectionHighlightLines, projectToWalls3d, projectToModel3d } from "./toWalls3d";
|
import { selectionHighlightLines, projectToWalls3d, projectToModel3d } from "./toWalls3d";
|
||||||
|
|
||||||
@@ -117,6 +118,44 @@ describe("Bauteil-/Materialfarbe statt Einheits-Grau (Wände + Decken)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Fenster-Glasscheiben (emitOpeningGlass)", () => {
|
||||||
|
it("1 Fenster + 1 Tür -> genau 1 Glas-Mesh mit korrekter vertikaler Ausdehnung", () => {
|
||||||
|
// Sample-Projekt auf EINE Öffnung (Fenster O1) + EINE Tür (D1) reduzieren,
|
||||||
|
// beide auf W1 (EG, baseElevation 0). Kontext ist leer -> `meshes` enthält
|
||||||
|
// ausschliesslich die Glasscheiben.
|
||||||
|
const p: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
openings: (sampleProject.openings ?? []).filter((o) => o.id === "O1"),
|
||||||
|
doors: sampleProject.doors,
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
const { meshes } = projectToModel3d(p);
|
||||||
|
// Genau EINE Scheibe: das Fenster O1; die Tür D1 bleibt offen (keine Scheibe).
|
||||||
|
expect(meshes.length).toBe(1);
|
||||||
|
const glass = meshes[0];
|
||||||
|
// Quader: 8 Ecken (24 Floats) + 12 Dreiecke (36 Indizes), leicht bläuliches Glas.
|
||||||
|
expect(glass.positions.length).toBe(24);
|
||||||
|
expect(glass.indices.length).toBe(36);
|
||||||
|
expect(glass.color).toEqual([0.62, 0.76, 0.85]);
|
||||||
|
|
||||||
|
// Vertikale Ausdehnung (z = 3. Komponente jeder Ecke, MODELL-Höhe):
|
||||||
|
// O1 sillHeight 0.9, height 1.2, Wand-UK 0 -> UK 0.9 .. OK 2.1.
|
||||||
|
const zs: number[] = [];
|
||||||
|
for (let i = 2; i < glass.positions.length; i += 3) zs.push(glass.positions[i]);
|
||||||
|
expect(Math.min(...zs)).toBeCloseTo(0.9, 6);
|
||||||
|
expect(Math.max(...zs)).toBeCloseTo(2.1, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Türen bekommen keine Scheibe; je Fenster genau eine", () => {
|
||||||
|
// Volles Sample-Projekt: 2 Fenster (O1, O2) + 1 Tür (D1), Kontext leer.
|
||||||
|
const { meshes } = projectToModel3d(sampleProject);
|
||||||
|
expect(meshes.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Bauteil-/Materialfarbe Decke", () => {
|
||||||
it("Decke -> Farbe der dicksten Schicht (dg-massiv: concrete 0.2 m, #9aa0a6)", () => {
|
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 ->
|
// Deckentyp "dg-massiv": screed 0.06, insulation 0.04, concrete 0.2 ->
|
||||||
// dickste Schicht = concrete (#9aa0a6).
|
// dickste Schicht = concrete (#9aa0a6).
|
||||||
|
|||||||
+72
-1
@@ -127,6 +127,10 @@ const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
|||||||
const TERRAIN_RGB: RRgb = [169 / 255, 176 / 255, 162 / 255];
|
const TERRAIN_RGB: RRgb = [169 / 255, 176 / 255, 162 / 255];
|
||||||
/** Neutrales Hellgrau für importierte Meshes (three.js mats.mesh = 0x9fa6ae). */
|
/** Neutrales Hellgrau für importierte Meshes (three.js mats.mesh = 0x9fa6ae). */
|
||||||
const IMPORTED_RGB: RRgb = [159 / 255, 166 / 255, 174 / 255];
|
const IMPORTED_RGB: RRgb = [159 / 255, 166 / 255, 174 / 255];
|
||||||
|
/** Helles, leicht bläuliches Fenster-Glas (Scheibe in der Öffnung). */
|
||||||
|
const GLASS_RGB: RRgb = [0.62, 0.76, 0.85];
|
||||||
|
/** Halbe Dicke der Fensterscheibe (Meter) — sehr dünn, mittig in der Wand. */
|
||||||
|
const GLASS_HALF_THICK = 0.01;
|
||||||
|
|
||||||
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
||||||
const EPS = 1e-4;
|
const EPS = 1e-4;
|
||||||
@@ -378,6 +382,70 @@ function emitMeshes(project: Project): RMesh[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emittiert je Fensteröffnung eine dünne Glasscheibe als rohes Quader-Mesh —
|
||||||
|
* damit Fenster im 3D als Fenster lesbar sind statt als blosse Löcher in der
|
||||||
|
* Wand. Nur `kind === "window"`; Türen bleiben offen (keine Scheibe).
|
||||||
|
*
|
||||||
|
* Für jede Scheibe wird ein geschlossener Quader (8 Ecken, 12 Dreiecke) gebaut:
|
||||||
|
* • entlang der Wandachse: das geklemmte Öffnungs-Intervall (openingInterval),
|
||||||
|
* • vertikal: die absolute Öffnungs-Ausdehnung (openingVerticalExtent, also
|
||||||
|
* UK = baseElevation+sillHeight … OK = +height, an den Wandkopf geklemmt) —
|
||||||
|
* exakt der Bereich, den emitWall als Loch freilässt,
|
||||||
|
* • quer zur Wand: ±GLASS_HALF_THICK um die Wandachse (mittig in der Dicke).
|
||||||
|
*
|
||||||
|
* `positions` liegen — wie bei {@link emitMeshes} (Terrain/Import) — in MODELL-
|
||||||
|
* Metern als flaches (x, y, z=Höhe)-Array; render3d bildet sie selbst nach world
|
||||||
|
* (x, Höhe, y) ab und rendert die Meshes doppelseitig (append_context_mesh),
|
||||||
|
* daher ist die Winding-Reihenfolge der Dreiecke unerheblich. `kind: "imported"`
|
||||||
|
* mit gesetzter `color` ⇒ render3d zeichnet die Scheibe in GLASS_RGB.
|
||||||
|
*/
|
||||||
|
function emitOpeningGlass(project: Project): RMesh[] {
|
||||||
|
// Faces eines Boxen-Quaders als Dreiecks-Indizes (Ecken-Index-Bits:
|
||||||
|
// bit0 = Achse from/to, bit1 = Höhe UK/OK, bit2 = Dicke −/+). Doppelseitig
|
||||||
|
// gerendert ⇒ Winding egal; je Fläche 2 Dreiecke, 6 Flächen = 12 Dreiecke.
|
||||||
|
const BOX_TRIS: number[] = [
|
||||||
|
0, 1, 3, 0, 3, 2, // Dicke −
|
||||||
|
4, 5, 7, 4, 7, 6, // Dicke +
|
||||||
|
0, 1, 5, 0, 5, 4, // UK
|
||||||
|
2, 3, 7, 2, 7, 6, // OK
|
||||||
|
0, 2, 6, 0, 6, 4, // from
|
||||||
|
1, 3, 7, 1, 7, 5, // to
|
||||||
|
];
|
||||||
|
const out: RMesh[] = [];
|
||||||
|
for (const wall of project.walls) {
|
||||||
|
const dx = wall.end.x - wall.start.x;
|
||||||
|
const dy = wall.end.y - wall.start.y;
|
||||||
|
const len = Math.hypot(dx, dy);
|
||||||
|
if (len < 1e-9) continue;
|
||||||
|
const ux = dx / len;
|
||||||
|
const uy = dy / len;
|
||||||
|
// Grundriss-Normale (Dicken-Achse) — dieselbe Konvention wie pushSegment.
|
||||||
|
const nx = uy;
|
||||||
|
const ny = -ux;
|
||||||
|
for (const op of openingsOfWall(project, wall.id)) {
|
||||||
|
if (op.kind !== "window") continue; // Türen bekommen keine Scheibe.
|
||||||
|
const iv = openingInterval(wall, op);
|
||||||
|
if (!iv) continue;
|
||||||
|
const { zBottom, zTop } = openingVerticalExtent(project, wall, op);
|
||||||
|
if (zTop - zBottom <= EPS) continue;
|
||||||
|
const positions: number[] = [];
|
||||||
|
// 8 Ecken: Achse s ∈ {from,to} × Höhe z ∈ {UK,OK} × Dicke t ∈ {−,+}.
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const s = i & 1 ? iv.to : iv.from;
|
||||||
|
const z = i & 2 ? zTop : zBottom;
|
||||||
|
const t = i & 4 ? GLASS_HALF_THICK : -GLASS_HALF_THICK;
|
||||||
|
const px = wall.start.x + ux * s + nx * t;
|
||||||
|
const py = wall.start.y + uy * s + ny * t;
|
||||||
|
// MODELL-Koordinaten (x, y, z=Höhe) wie emitMeshes.
|
||||||
|
positions.push(px, py, z);
|
||||||
|
}
|
||||||
|
out.push({ positions, indices: [...BOX_TRIS], kind: "imported", color: GLASS_RGB });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
|
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
|
||||||
* mit Öffnungen in Teilquader.
|
* mit Öffnungen in Teilquader.
|
||||||
@@ -393,7 +461,10 @@ export function projectToModel3d(project: Project): RModel3d {
|
|||||||
return {
|
return {
|
||||||
walls: projectToWalls3d(project),
|
walls: projectToWalls3d(project),
|
||||||
slabs: emitSlabs(project),
|
slabs: emitSlabs(project),
|
||||||
meshes: emitMeshes(project),
|
// Kontext-Meshes (Terrain/Import) + Fenster-Glasscheiben (eigenes Mesh je
|
||||||
|
// Fensteröffnung) — pickGeometry/Highlight nutzen `meshes` bewusst NICHT,
|
||||||
|
// daher sind die Scheiben nicht selektierbar (man wählt Wand/Öffnung).
|
||||||
|
meshes: [...emitMeshes(project), ...emitOpeningGlass(project)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user