Sonnenschutz 3D: Rollladenkasten als Box über der Öffnung
emitOpeningShading zeichnet für Fenster mit shading.create eine helle 3D-Box über der Öffnungs-OK, an der Aussenfläche ansetzend und um box.depth hinaus, box.height hoch, box.width breit (0 ⇒ Öffnungsbreite). Nur mittel/fein. Vervollständigt das Sonnenschutz-Feature (2D-Kontur war da, 3D-Box war in der Studie 'später'). +3 Tests (Box-Primitive/Position, Leerfälle, grob). 3D visuell vom Nutzer in der Tauri-App zu prüfen. 662/662 grün.
This commit is contained in:
@@ -1195,3 +1195,69 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
|
|||||||
expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(1);
|
expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("emitOpeningShading (Rollladen-/Sonnenschutzkasten als 3D-Box)", () => {
|
||||||
|
// SHADING_RGB in toWalls3d.ts — nur zur Isolation der Kasten-Meshes.
|
||||||
|
const isShadingMesh = (m: { color: readonly number[] }) =>
|
||||||
|
m.color[0] === 0.88 && m.color[1] === 0.88 && m.color[2] === 0.9;
|
||||||
|
|
||||||
|
const withShading = (create: boolean): Project => ({
|
||||||
|
...sampleProject,
|
||||||
|
windowTypes: [
|
||||||
|
{
|
||||||
|
id: "win-shade",
|
||||||
|
name: "Test Rollladen",
|
||||||
|
kind: "fest",
|
||||||
|
wingCount: 1,
|
||||||
|
glazing: "einfach",
|
||||||
|
frameThickness: 0.06,
|
||||||
|
frameWidth: 0.06,
|
||||||
|
defaultSillHeight: 0.9,
|
||||||
|
defaultWidth: 1.0,
|
||||||
|
defaultHeight: 1.2,
|
||||||
|
shading: create
|
||||||
|
? { create: true, kind: "rollladen", box: { width: 0, height: 0.24, depth: 0.2 } }
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
openings: [
|
||||||
|
{
|
||||||
|
id: "OS",
|
||||||
|
type: "opening",
|
||||||
|
hostWallId: "W1",
|
||||||
|
categoryCode: "21",
|
||||||
|
kind: "window",
|
||||||
|
typeId: "win-shade",
|
||||||
|
position: 3.0,
|
||||||
|
width: 1.0,
|
||||||
|
height: 1.2,
|
||||||
|
sillHeight: 0.9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
doors: [],
|
||||||
|
context: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shading.create -> genau eine Kasten-Box (Quader: 24 Floats, 36 Indizes)", () => {
|
||||||
|
const meshes = projectToModel3d(withShading(true)).meshes.filter(isShadingMesh);
|
||||||
|
expect(meshes.length).toBe(1);
|
||||||
|
expect(meshes[0].positions.length).toBe(24);
|
||||||
|
expect(meshes[0].indices.length).toBe(36);
|
||||||
|
// Der Kasten sitzt ÜBER der Öffnungs-OK (UK 0.9 + Höhe 1.2 = 2.1): alle
|
||||||
|
// z-Werte >= 2.1 (nur nach oben), OK bei 2.1 + 0.24 = 2.34.
|
||||||
|
const zs: number[] = [];
|
||||||
|
for (let i = 2; i < meshes[0].positions.length; i += 3) zs.push(meshes[0].positions[i]);
|
||||||
|
expect(Math.min(...zs)).toBeCloseTo(2.1, 6);
|
||||||
|
expect(Math.max(...zs)).toBeCloseTo(2.34, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kein shading / create:false -> keine Kasten-Box", () => {
|
||||||
|
expect(projectToModel3d(withShading(false)).meshes.filter(isShadingMesh).length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("grob -> keine Kasten-Box (nur mittel/fein)", () => {
|
||||||
|
expect(
|
||||||
|
projectToModel3d(withShading(true), { detail: "grob" }).meshes.filter(isShadingMesh).length,
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ const GLASS_HALF_THICK = 0.01;
|
|||||||
* eigenes Bauteil lesbar ist (s. {@link emitOpeningFrames}).
|
* eigenes Bauteil lesbar ist (s. {@link emitOpeningFrames}).
|
||||||
*/
|
*/
|
||||||
const FRAME_RGB: RRgb = [0.85, 0.85, 0.83];
|
const FRAME_RGB: RRgb = [0.85, 0.85, 0.83];
|
||||||
|
/** Heller Grauton für den Rollladen-/Sonnenschutzkasten (s. {@link emitOpeningShading}). */
|
||||||
|
const SHADING_RGB: RRgb = [0.88, 0.88, 0.9];
|
||||||
/** Default-Ansichtsbreite des Rahmenprofils (Meter), wenn `TYPE.frameWidth` fehlt. */
|
/** Default-Ansichtsbreite des Rahmenprofils (Meter), wenn `TYPE.frameWidth` fehlt. */
|
||||||
const DEFAULT_FRAME_WIDTH = 0.06;
|
const DEFAULT_FRAME_WIDTH = 0.06;
|
||||||
/** Warmer Terrakotta-Grauton für Dachflächen/-giebel (s. {@link emitRoofs}) — hebt das Dach klar von Wand/Decke/Kontext ab. */
|
/** Warmer Terrakotta-Grauton für Dachflächen/-giebel (s. {@link emitRoofs}) — hebt das Dach klar von Wand/Decke/Kontext ab. */
|
||||||
@@ -2108,6 +2110,54 @@ function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMes
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollladen-/Sonnenschutzkasten als 3D-Box über der Öffnung (nur Fenster mit
|
||||||
|
* `WindowType.shading.create`, nur mittel/fein). Ein heller Quader, aussen an
|
||||||
|
* der Aussenfläche (params.nMax) ansetzend und um `box.depth` nach aussen
|
||||||
|
* ragend, von der Öffnungs-OK um `box.height` nach oben. Breite = `box.width`
|
||||||
|
* (0/fehlt ⇒ Öffnungsbreite), mittig über der Öffnung. Pendant zur 2D-Kontur
|
||||||
|
* (`window-shading` in generatePlan). 3D vom Nutzer in der Tauri-App zu prüfen.
|
||||||
|
*/
|
||||||
|
function emitOpeningShading(project: Project, detail: DetailLevel = "fein"): RMesh[] {
|
||||||
|
if (detail === "grob") return [];
|
||||||
|
const out: RMesh[] = [];
|
||||||
|
for (const wall of project.walls) {
|
||||||
|
for (const op of openingsOfWall(project, wall.id)) {
|
||||||
|
if (op.kind !== "window") continue;
|
||||||
|
const wt = getWindowType(project, op);
|
||||||
|
if (!wt?.shading?.create) continue;
|
||||||
|
const iv = openingInterval(wall, op);
|
||||||
|
if (!iv) continue;
|
||||||
|
const vert = openingVerticalExtent(project, wall, op);
|
||||||
|
if (vert.zTop - vert.zBottom <= EPS) continue;
|
||||||
|
const params = resolveOpeningFrame(project, wall, op);
|
||||||
|
if (!params) continue;
|
||||||
|
const box = wt.shading.box;
|
||||||
|
const h = Math.max(0, box.height);
|
||||||
|
const d = Math.max(0, box.depth);
|
||||||
|
if (h <= EPS || d <= EPS) continue;
|
||||||
|
const span = box.width > 0 ? box.width : iv.to - iv.from;
|
||||||
|
const center = (iv.from + iv.to) / 2;
|
||||||
|
const from = center - span / 2;
|
||||||
|
const to = center + span / 2;
|
||||||
|
// Aussen an der Aussenfläche (nMax) ansetzen und um die Kastentiefe hinaus.
|
||||||
|
const nOuter = params.nMax;
|
||||||
|
const mesh = openingAxisBox(
|
||||||
|
wall,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
vert.zTop,
|
||||||
|
vert.zTop + h,
|
||||||
|
nOuter,
|
||||||
|
nOuter + d,
|
||||||
|
SHADING_RGB,
|
||||||
|
);
|
||||||
|
if (mesh) out.push(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.
|
||||||
@@ -2189,6 +2239,7 @@ export function projectToModel3d(project: Project, opts: Model3dOptions = {}): R
|
|||||||
...emitMeshes(project),
|
...emitMeshes(project),
|
||||||
...emitOpeningGlass(project),
|
...emitOpeningGlass(project),
|
||||||
...emitOpeningFrames(project, detail),
|
...emitOpeningFrames(project, detail),
|
||||||
|
...emitOpeningShading(project, detail),
|
||||||
...emitStairMeshes(project),
|
...emitStairMeshes(project),
|
||||||
...emitRoofs(project),
|
...emitRoofs(project),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user