3D-Öffnungen im VW-Detail 'fein': verschachtelte Flügelrahmen, DIN-Symbole, Sims, Zargen-Umgriff
- Flügelrahmen sitzen in eigenem, ~12 mm nach aussen vorstehenden und flacheren Quer-Band (abgesetzte, plastische Schachtelung Blendrahmen -> Flügelrahmen -> Glas) statt flach mit dem Blendrahmen verschmolzen; jetzt bei mittel + fein. - DIN-Öffnungssymbole (Dreh/Kipp/Dreh-Kipp) als sehr dünne, in der Öffnungsebene gedrehte dunkle Prismen knapp vor der Glasebene (neuer Helfer openingPlaneBox); nur bei fein, nur je nicht-festem Flügel, exakt wie in der 2D-Ansicht. - Fensterbank (Sims) unter der Öffnung: Bank-Quader + dünnere Tropfkanten-Stufe, seitlich ueberstehend, nach aussen auskragend; Gating sillBoard (keine/innen aus, Default zeichnen) und DetailLevel (mittel + fein). - Tuer-Zarge mit Umgriff: schmale Bekleidungs-Platte vor beiden Wandflaechen als Ring um die Oeffnung (nur frameKind 'zarge'; Blockrahmen bleibt buendig). - Mesh-Tests je Feature: Anzahl/Farbe/BBox, sillBoard- und frameKind-Gating, Symbole nur fein/nicht-fest, Flügelrahmen-Vorstand.
This commit is contained in:
+170
-4
@@ -406,7 +406,7 @@ describe("Rahmen-/Sprossen-Riegel typisierter Öffnungen (emitOpeningFrames)", (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Detailgrad steuert Rahmen/Sprossen im 3D (grob=0, mittel=Rahmen, fein=+Mittelpfosten+Flügelrahmen)", () => {
|
it("Detailgrad steuert Rahmen/Sprossen im 3D (grob=0, mittel=Rahmen+Flügel, fein=+Mittelpfosten+Sprossen)", () => {
|
||||||
// O1 mit erzwungenem wingCount 2 (window-standard "drehkipp", öffenbar).
|
// O1 mit erzwungenem wingCount 2 (window-standard "drehkipp", öffenbar).
|
||||||
const o1 = (sampleProject.openings ?? []).find((o) => o.id === "O1");
|
const o1 = (sampleProject.openings ?? []).find((o) => o.id === "O1");
|
||||||
if (!o1) throw new Error("O1 fehlt im sampleProject");
|
if (!o1) throw new Error("O1 fehlt im sampleProject");
|
||||||
@@ -419,9 +419,10 @@ describe("Rahmen-/Sprossen-Riegel typisierter Öffnungen (emitOpeningFrames)", (
|
|||||||
const frameCount = (detail: "grob" | "mittel" | "fein"): number =>
|
const frameCount = (detail: "grob" | "mittel" | "fein"): number =>
|
||||||
projectToModel3d(p, { detail }).meshes.filter(isFrame).length;
|
projectToModel3d(p, { detail }).meshes.filter(isFrame).length;
|
||||||
expect(frameCount("grob")).toBe(0); // keine Rahmen/Sprossen
|
expect(frameCount("grob")).toBe(0); // keine Rahmen/Sprossen
|
||||||
expect(frameCount("mittel")).toBe(4); // 2 Pfosten + Sturz + Schwelle (keine Sprossen/Flügelrahmen)
|
// mittel: 4 Basis (2 Pfosten + Sturz + Schwelle) + 2 öffenbare Flügel × 4
|
||||||
// fein: 4 Basis + 1 Mittelpfosten (wingCount 2) + 2 öffenbare Flügel × 4
|
// verschachtelte Flügelrahmen-Riegel = 4 + 8 = 12 (keine Mittelpfosten/Sprossen).
|
||||||
// Flügelrahmen-Riegel = 4 + 1 + 8 = 13.
|
expect(frameCount("mittel")).toBe(12);
|
||||||
|
// fein: mittel + 1 Flügel-Mittelpfosten (wingCount 2) = 12 + 1 = 13.
|
||||||
expect(frameCount("fein")).toBe(13);
|
expect(frameCount("fein")).toBe(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -753,6 +754,171 @@ describe("Rahmen-/Sprossen-Riegel typisierter Öffnungen (emitOpeningFrames)", (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("VW-fein-Öffnungsdetails: Fensterbank, DIN-Symbole, verschachtelte Flügelrahmen, Zargen-Umgriff", () => {
|
||||||
|
const isFrame = (m: { color: [number, number, number] }): boolean =>
|
||||||
|
m.color[0] === 0.85 && m.color[1] === 0.85 && m.color[2] === 0.83;
|
||||||
|
// SILL_RGB [0.8,0.8,0.78] und SYMBOL_RGB [0.25,0.27,0.3] sind bewusst nicht
|
||||||
|
// exportiert -> Filter per Literalwert, wie bei GLASS_RGB/FRAME_RGB.
|
||||||
|
const isSill = (m: { color: [number, number, number] }): boolean =>
|
||||||
|
m.color[0] === 0.8 && m.color[1] === 0.8 && m.color[2] === 0.78;
|
||||||
|
const isSymbol = (m: { color: [number, number, number] }): boolean =>
|
||||||
|
m.color[0] === 0.25 && m.color[1] === 0.27 && m.color[2] === 0.3;
|
||||||
|
|
||||||
|
// Ein Fenster (O1-Geometrie) mit gegebenem Fenstertyp auf W1.
|
||||||
|
const winProject = (
|
||||||
|
wt: NonNullable<Project["windowTypes"]>[number],
|
||||||
|
): Project => ({
|
||||||
|
...sampleProject,
|
||||||
|
windowTypes: [...(sampleProject.windowTypes ?? []), wt],
|
||||||
|
openings: [
|
||||||
|
{
|
||||||
|
id: "OS",
|
||||||
|
type: "opening",
|
||||||
|
hostWallId: "W1",
|
||||||
|
categoryCode: "21",
|
||||||
|
kind: "window",
|
||||||
|
typeId: wt.id,
|
||||||
|
position: 3.0,
|
||||||
|
width: 1.0,
|
||||||
|
height: 1.2,
|
||||||
|
sillHeight: 0.9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
doors: [],
|
||||||
|
context: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseWin = {
|
||||||
|
id: "win-detail-test",
|
||||||
|
name: "Test Detail",
|
||||||
|
kind: "fest" as const,
|
||||||
|
wingCount: 1,
|
||||||
|
glazing: "einfach" as const,
|
||||||
|
frameThickness: 0.06,
|
||||||
|
frameWidth: 0.06,
|
||||||
|
defaultSillHeight: 0.9,
|
||||||
|
defaultWidth: 1.0,
|
||||||
|
defaultHeight: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("Fensterbank (Sims): 2 Bänder (Bank + Tropfkante) unter der Öffnung, Gating sillBoard + DetailLevel", () => {
|
||||||
|
// Default (kein sillBoard) -> zeichnen (wie 2D-Ansicht): 2 Sims-Bänder.
|
||||||
|
const pDefault = winProject({ ...baseWin });
|
||||||
|
const sills = projectToModel3d(pDefault).meshes.filter(isSill);
|
||||||
|
expect(sills.length).toBe(2);
|
||||||
|
// Beide Bänder liegen UNTER der Öffnungs-UK (sillHeight 0.9).
|
||||||
|
for (const s of sills) {
|
||||||
|
for (let i = 2; i < s.positions.length; i += 3) {
|
||||||
|
expect(s.positions[i]).toBeLessThanOrEqual(0.9 + 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sillBoard-Gating: "keine"/"innen" -> keine Bank; "aussen"/"beide" -> Bank.
|
||||||
|
const count = (sb: "keine" | "innen" | "aussen" | "beide"): number =>
|
||||||
|
projectToModel3d(winProject({ ...baseWin, id: `win-sb-${sb}`, sillBoard: sb })).meshes.filter(isSill).length;
|
||||||
|
expect(count("keine")).toBe(0);
|
||||||
|
expect(count("innen")).toBe(0);
|
||||||
|
expect(count("aussen")).toBe(2);
|
||||||
|
expect(count("beide")).toBe(2);
|
||||||
|
// DetailLevel: grob -> keine Bank; mittel/fein -> Bank.
|
||||||
|
const byDetail = (detail: "grob" | "mittel" | "fein"): number =>
|
||||||
|
projectToModel3d(pDefault, { detail }).meshes.filter(isSill).length;
|
||||||
|
expect(byDetail("grob")).toBe(0);
|
||||||
|
expect(byDetail("mittel")).toBe(2);
|
||||||
|
expect(byDetail("fein")).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DIN-Öffnungssymbole: nur fein, nur je NICHT-festem Flügel; Dreh=V, Kipp=V, Dreh-Kipp=beide", () => {
|
||||||
|
// Dreh-Kipp (öffenbar): Dreh (2 Diagonalen) + Kipp (2 Diagonalen) = 4 Symbol-Boxen.
|
||||||
|
const pDrehkipp = winProject({ ...baseWin, id: "win-drehkipp", kind: "drehkipp" });
|
||||||
|
expect(projectToModel3d(pDrehkipp).meshes.filter(isSymbol).length).toBe(4);
|
||||||
|
// Dreh -> 2, Kipp -> 2.
|
||||||
|
expect(
|
||||||
|
projectToModel3d(winProject({ ...baseWin, id: "win-dreh", kind: "dreh" })).meshes.filter(isSymbol).length,
|
||||||
|
).toBe(2);
|
||||||
|
expect(
|
||||||
|
projectToModel3d(winProject({ ...baseWin, id: "win-kipp", kind: "kipp" })).meshes.filter(isSymbol).length,
|
||||||
|
).toBe(2);
|
||||||
|
// Fest -> kein Symbol.
|
||||||
|
expect(
|
||||||
|
projectToModel3d(winProject({ ...baseWin, id: "win-fest2", kind: "fest" })).meshes.filter(isSymbol).length,
|
||||||
|
).toBe(0);
|
||||||
|
// Symbole nur bei „fein" (mittel/grob: keine).
|
||||||
|
expect(projectToModel3d(pDrehkipp, { detail: "mittel" }).meshes.filter(isSymbol).length).toBe(0);
|
||||||
|
expect(projectToModel3d(pDrehkipp, { detail: "grob" }).meshes.filter(isSymbol).length).toBe(0);
|
||||||
|
// Symbol-Boxen sind sehr dünn (endliche, gültige Positionen) und liegen im
|
||||||
|
// Öffnungs-Höhenintervall (UK 0.9 .. OK 2.1).
|
||||||
|
for (const sym of projectToModel3d(pDrehkipp).meshes.filter(isSymbol)) {
|
||||||
|
expect(sym.positions.every((v) => Number.isFinite(v))).toBe(true);
|
||||||
|
for (let i = 2; i < sym.positions.length; i += 3) {
|
||||||
|
expect(sym.positions[i]).toBeGreaterThanOrEqual(0.9 - 1e-6);
|
||||||
|
expect(sym.positions[i]).toBeLessThanOrEqual(2.1 + 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Verschachtelter Flügelrahmen steht ~12 mm vor dem Blendrahmen (nicht mehr flach)", () => {
|
||||||
|
// Fest vs. öffenbar, sonst identisch (flush, frameThickness gleich). Der
|
||||||
|
// Flügelrahmen des öffenbaren Fensters sitzt in einem nach aussen (nMax)
|
||||||
|
// vorstehenden Band -> seine Aussenkante ragt 0.012 m weiter hinaus als der
|
||||||
|
// Blendrahmen (bei W1 in Richtung negatives world-y, s. Inset-Test).
|
||||||
|
const minY = (p: Project): number => {
|
||||||
|
let m = Infinity;
|
||||||
|
for (const mesh of projectToModel3d(p).meshes.filter(isFrame)) {
|
||||||
|
for (let i = 1; i < mesh.positions.length; i += 3) m = Math.min(m, mesh.positions[i]);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
const fixedMinY = minY(winProject({ ...baseWin, id: "win-proud-fixed", kind: "fest", sillBoard: "keine" }));
|
||||||
|
const openMinY = minY(winProject({ ...baseWin, id: "win-proud-open", kind: "drehkipp", sillBoard: "keine" }));
|
||||||
|
expect(fixedMinY - openMinY).toBeCloseTo(0.012, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Tür-Zarge mit Umgriff: zarge -> 6 Bekleidungs-Riegel mehr als blockrahmen", () => {
|
||||||
|
const doorProject = (frameKind: "zarge" | "blockrahmen"): Project => ({
|
||||||
|
...sampleProject,
|
||||||
|
doorTypes: [
|
||||||
|
{
|
||||||
|
id: `door-${frameKind}-test`,
|
||||||
|
name: `Test ${frameKind}`,
|
||||||
|
kind: "dreh",
|
||||||
|
leafCount: 1,
|
||||||
|
leafStyle: "glatt",
|
||||||
|
frameThickness: 0.05,
|
||||||
|
frameKind,
|
||||||
|
frameWidth: 0.06,
|
||||||
|
defaultWidth: 0.9,
|
||||||
|
defaultHeight: 2.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
openings: [
|
||||||
|
{
|
||||||
|
id: "DT",
|
||||||
|
type: "opening",
|
||||||
|
hostWallId: "W1",
|
||||||
|
categoryCode: "31",
|
||||||
|
kind: "door",
|
||||||
|
typeId: `door-${frameKind}-test`,
|
||||||
|
position: 3.0,
|
||||||
|
width: 0.9,
|
||||||
|
height: 2.0,
|
||||||
|
sillHeight: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
doors: [],
|
||||||
|
context: [],
|
||||||
|
});
|
||||||
|
const frames = (frameKind: "zarge" | "blockrahmen"): number =>
|
||||||
|
projectToModel3d(doorProject(frameKind)).meshes.filter(isFrame).length;
|
||||||
|
// Zarge: Basis-Rahmen (2 Pfosten + Sturz, kein threshold) + 2 Wandseiten × 3
|
||||||
|
// Bekleidungs-Riegel = 3 + 6 = 9. Blockrahmen: nur Basis = 3.
|
||||||
|
expect(frames("blockrahmen")).toBe(3);
|
||||||
|
expect(frames("zarge")).toBe(9);
|
||||||
|
// Umgriff nur mittel/fein, nicht grob.
|
||||||
|
expect(projectToModel3d(doorProject("zarge"), { detail: "grob" }).meshes.filter(isFrame).length).toBe(0);
|
||||||
|
expect(projectToModel3d(doorProject("zarge"), { detail: "mittel" }).meshes.filter(isFrame).length).toBe(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Per-Schicht-Decken-Dominanz Wand/Decke (3D-Entsprechung der Schnitt-Boolean-Dominanz)", () => {
|
describe("Per-Schicht-Decken-Dominanz Wand/Decke (3D-Entsprechung der Schnitt-Boolean-Dominanz)", () => {
|
||||||
// Sample-Projekt: W1-W4 (EG, Wandtyp "aw": render-ext 10, insulation 20,
|
// Sample-Projekt: W1-W4 (EG, Wandtyp "aw": render-ext 10, insulation 20,
|
||||||
// brick 50, render-int 10) tragen Wandhöhe 2.6 m = Geschosshöhe. Decke C1
|
// brick 50, render-int 10) tragen Wandhöhe 2.6 m = Geschosshöhe. Decke C1
|
||||||
|
|||||||
+220
-26
@@ -401,6 +401,22 @@ const GLASS_HALF_THICK = 0.01;
|
|||||||
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}). */
|
/** Heller Grauton für den Rollladen-/Sonnenschutzkasten (s. {@link emitOpeningShading}). */
|
||||||
const SHADING_RGB: RRgb = [0.88, 0.88, 0.9];
|
const SHADING_RGB: RRgb = [0.88, 0.88, 0.9];
|
||||||
|
/**
|
||||||
|
* Heller Stein-/Beton-Grauton der Fensterbank (Sims/Tropfkante) — bewusst etwas
|
||||||
|
* dunkler/kühler als der Rahmen ({@link FRAME_RGB}), damit die Bank im 3D als
|
||||||
|
* eigenes Bauteil (nicht als Rahmenfortsatz) liest (s. Sims-Zweig in
|
||||||
|
* {@link frameMeshesForOpening}).
|
||||||
|
*/
|
||||||
|
const SILL_RGB: RRgb = [0.8, 0.8, 0.78];
|
||||||
|
/**
|
||||||
|
* Dunkles Grau der DIN-Öffnungssymbole (dünne Diagonalen AUF dem Glas, wie in der
|
||||||
|
* 2D-Ansicht) — feine dunkle Linien auf der Scheibe (s. Symbol-Zweig in
|
||||||
|
* {@link frameMeshesForOpening}). Als sehr dünne, in der Öffnungsebene gedrehte
|
||||||
|
* Prismen ({@link openingPlaneBox}) knapp vor der Glasebene.
|
||||||
|
*/
|
||||||
|
const SYMBOL_RGB: RRgb = [0.25, 0.27, 0.3];
|
||||||
|
/** Halbe Breite der Symbol-Diagonalen (Meter) — ~5 mm Gesamtbreite, feine Linie. */
|
||||||
|
const SYMBOL_HALF_WIDTH = 0.0025;
|
||||||
/** 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. */
|
||||||
@@ -1872,6 +1888,62 @@ function openingAxisBox(
|
|||||||
return { positions, indices: [...OPENING_BOX_TRIS], kind: "imported", color };
|
return { positions, indices: [...OPENING_BOX_TRIS], kind: "imported", color };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut EIN sehr dünnes Prisma mit FREI orientierter Längsachse IN der Öffnungs-
|
||||||
|
* ebene — die diagonale Schwester von {@link openingAxisBox}. Die Längsachse
|
||||||
|
* verläuft vom Punkt `(u0,h0)` zum Punkt `(u1,h1)`, beide in Öffnungsebenen-
|
||||||
|
* koordinaten: `u` = Meter entlang der Wandrichtung (ab `wall.start`), `h` =
|
||||||
|
* absolute Welthöhe (Meter). Der Querschnitt spannt `±halfWidth` senkrecht zur
|
||||||
|
* Längsachse INNERHALB der Ebene (gedreht mit) und `[nBack,nFront]` entlang der
|
||||||
|
* Grundriss-Normalen (quer zur Wand, Tiefe). Damit lassen sich die schrägen
|
||||||
|
* DIN-Öffnungssymbole (Dreh-/Kipp-Diagonalen) als dünne, gedrehte dunkle Riegel
|
||||||
|
* AUF dem Glas erzeugen — der Renderer kennt nur Dreiecks-Meshes, keine Linien.
|
||||||
|
* `null` bei entarteter Länge (≤ EPS). 8 Ecken, dieselbe {@link OPENING_BOX_TRIS}-
|
||||||
|
* Topologie (doppelseitig gerendert ⇒ Winding egal; die drei Bits parametrisieren
|
||||||
|
* hier Endpunkt/Quer-Versatz/Tiefe).
|
||||||
|
*/
|
||||||
|
function openingPlaneBox(
|
||||||
|
wall: Wall,
|
||||||
|
u0: number,
|
||||||
|
h0: number,
|
||||||
|
u1: number,
|
||||||
|
h1: number,
|
||||||
|
halfWidth: number,
|
||||||
|
nFront: number,
|
||||||
|
nBack: number,
|
||||||
|
color: RRgb,
|
||||||
|
): RMesh | null {
|
||||||
|
const du = u1 - u0;
|
||||||
|
const dh = h1 - h0;
|
||||||
|
const segLen = Math.hypot(du, dh);
|
||||||
|
if (segLen < EPS) return null;
|
||||||
|
const dx = wall.end.x - wall.start.x;
|
||||||
|
const dy = wall.end.y - wall.start.y;
|
||||||
|
const wlen = Math.hypot(dx, dy);
|
||||||
|
if (wlen < 1e-9) return null;
|
||||||
|
const ux = dx / wlen;
|
||||||
|
const uy = dy / wlen;
|
||||||
|
// Grundriss-Normale (Dicken-Achse) — dieselbe Konvention wie openingAxisBox.
|
||||||
|
const nx = uy;
|
||||||
|
const ny = -ux;
|
||||||
|
// In der Ebene senkrecht zur Längsachse (u entlang Wand, h = Welthöhe):
|
||||||
|
// Rotation der normierten Richtung um 90°. `pu` wirkt auf die u-Achse (Wand),
|
||||||
|
// `ph` auf die Höhe — beide in Metern, orthonormal ⇒ echte `halfWidth`-Breite.
|
||||||
|
const pu = -dh / segLen;
|
||||||
|
const ph = du / segLen;
|
||||||
|
const positions: number[] = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const ui = i & 1 ? u1 : u0;
|
||||||
|
const hi = i & 1 ? h1 : h0;
|
||||||
|
const s = i & 2 ? 1 : -1;
|
||||||
|
const t = i & 4 ? nFront : nBack;
|
||||||
|
const u = ui + s * halfWidth * pu;
|
||||||
|
const h = hi + s * halfWidth * ph;
|
||||||
|
positions.push(wall.start.x + ux * u + nx * t, wall.start.y + uy * u + ny * t, h);
|
||||||
|
}
|
||||||
|
return { positions, indices: [...OPENING_BOX_TRIS], kind: "imported", color };
|
||||||
|
}
|
||||||
|
|
||||||
/** Klemmt `v` auf `[lo,hi]`. */
|
/** Klemmt `v` auf `[lo,hi]`. */
|
||||||
function clampRange(v: number, lo: number, hi: number): number {
|
function clampRange(v: number, lo: number, hi: number): number {
|
||||||
return Math.max(lo, Math.min(hi, v));
|
return Math.max(lo, Math.min(hi, v));
|
||||||
@@ -1967,6 +2039,18 @@ interface OpeningFrameParams {
|
|||||||
* (Schachtelung Blendrahmen → Flügelrahmen → Glas). Türen: leer.
|
* (Schachtelung Blendrahmen → Flügelrahmen → Glas). Türen: leer.
|
||||||
*/
|
*/
|
||||||
sashes: SashDef[];
|
sashes: SashDef[];
|
||||||
|
/** Halbe Wanddicke (Meter) — Bezug der Wand-Aussenfläche (+n) für Bank/Zargen-Umgriff. */
|
||||||
|
halfThickness: number;
|
||||||
|
/**
|
||||||
|
* Fensterbank (Sims) zeichnen: nur Fenster mit `sillBoard` ≠ "keine"/"innen"
|
||||||
|
* (fehlt ⇒ zeichnen, wie in der 2D-Ansicht). Türen: nie.
|
||||||
|
*/
|
||||||
|
drawSill: boolean;
|
||||||
|
/**
|
||||||
|
* Zargen-Umgriff (Bekleidung VOR beiden Wandflächen) zeichnen: nur Türen mit
|
||||||
|
* `frameKind === "zarge"`. Blockrahmen bleibt bündig (false).
|
||||||
|
*/
|
||||||
|
zargeCladding: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): OpeningFrameParams | null {
|
function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): OpeningFrameParams | null {
|
||||||
@@ -1997,6 +2081,10 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
// Teilverglasung: glazingRatio (Glasanteil oben) bei Glasblatt; sonst voll.
|
// Teilverglasung: glazingRatio (Glasanteil oben) bei Glasblatt; sonst voll.
|
||||||
glazedFraction: Math.max(0.05, Math.min(1, dt.glazingRatio ?? 1)),
|
glazedFraction: Math.max(0.05, Math.min(1, dt.glazingRatio ?? 1)),
|
||||||
sashes: [],
|
sashes: [],
|
||||||
|
halfThickness,
|
||||||
|
drawSill: false,
|
||||||
|
// Zarge umgreift die Wand (Bekleidung auf beiden Flächen); Blockrahmen bleibt bündig.
|
||||||
|
zargeCladding: (dt.frameKind ?? "zarge") === "zarge",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const wt = getWindowType(project, op);
|
const wt = getWindowType(project, op);
|
||||||
@@ -2037,6 +2125,10 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
glazingPanes: glazingPanesOf(wt),
|
glazingPanes: glazingPanesOf(wt),
|
||||||
glazedFraction: 1,
|
glazedFraction: 1,
|
||||||
sashes,
|
sashes,
|
||||||
|
halfThickness,
|
||||||
|
// Fensterbank zeichnen, ausser explizit "keine"/"innen" (fehlt ⇒ zeichnen).
|
||||||
|
drawSill: wt.sillBoard !== "keine" && wt.sillBoard !== "innen",
|
||||||
|
zargeCladding: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2046,21 +2138,44 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
* ein Kämpfer-Riegel (Oberlicht-Trennung, `transomHeight>0`) sowie die
|
* ein Kämpfer-Riegel (Oberlicht-Trennung, `transomHeight>0`) sowie die
|
||||||
* Flügel-Mittelpfosten (`wingCount−1`, vertikal) und Kämpfer-Zeilen
|
* Flügel-Mittelpfosten (`wingCount−1`, vertikal) und Kämpfer-Zeilen
|
||||||
* (`mullionRows−1`, horizontal) im Flügel-Bereich zwischen Schwelle/Sturz
|
* (`mullionRows−1`, horizontal) im Flügel-Bereich zwischen Schwelle/Sturz
|
||||||
* bzw. Schwelle/Kämpfer. Alle Riegel in {@link FRAME_RGB}.
|
* bzw. Schwelle/Kämpfer. Alle Rahmen-Riegel in {@link FRAME_RGB}.
|
||||||
|
*
|
||||||
|
* DetailLevel-Staffelung (VW-Detailgrad „fein" als Referenz):
|
||||||
|
* • „grob" — wird gar nicht aufgerufen (s. {@link emitOpeningFrames}).
|
||||||
|
* • „mittel"— Blendrahmen + Oberlicht-Kämpfer + Flügelrahmen (verschachtelt) +
|
||||||
|
* Fensterbank/Zargen-Umgriff, aber KEINE Sprossen und KEINE DIN-Symbole.
|
||||||
|
* • „fein" — zusätzlich Flügel-Mittelpfosten + Kämpfer-Zeilen (Sprossen) und
|
||||||
|
* die DIN-Öffnungssymbole ({@link openingPlaneBox}) auf dem Glas.
|
||||||
*/
|
*/
|
||||||
function frameMeshesForOpening(
|
function frameMeshesForOpening(
|
||||||
wall: Wall,
|
wall: Wall,
|
||||||
iv: { from: number; to: number },
|
iv: { from: number; to: number },
|
||||||
vert: { zBottom: number; zTop: number },
|
vert: { zBottom: number; zTop: number },
|
||||||
params: OpeningFrameParams,
|
params: OpeningFrameParams,
|
||||||
fine = true,
|
detail: DetailLevel = "fein",
|
||||||
): RMesh[] {
|
): RMesh[] {
|
||||||
const out: RMesh[] = [];
|
const out: RMesh[] = [];
|
||||||
|
const fine = detail === "fein";
|
||||||
|
const medium = detail === "mittel";
|
||||||
|
const sashesOn = fine || medium; // Flügelrahmen: mittel + fein
|
||||||
|
const detailBodies = fine || medium; // Bank + Zargen-Umgriff: mittel + fein
|
||||||
const fw = params.frameWidth;
|
const fw = params.frameWidth;
|
||||||
const push = (from: number, to: number, zBottom: number, zTop: number): void => {
|
// Generischer Riegel mit frei wählbarem Quer-Band [nA,nB] und Farbe.
|
||||||
const mesh = openingAxisBox(wall, from, to, zBottom, zTop, params.nMin, params.nMax, FRAME_RGB);
|
const pushC = (
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
zBottom: number,
|
||||||
|
zTop: number,
|
||||||
|
nA: number,
|
||||||
|
nB: number,
|
||||||
|
color: RRgb,
|
||||||
|
): void => {
|
||||||
|
const mesh = openingAxisBox(wall, from, to, zBottom, zTop, nA, nB, color);
|
||||||
if (mesh) out.push(mesh);
|
if (mesh) out.push(mesh);
|
||||||
};
|
};
|
||||||
|
// Blendrahmen-Riegel (volles Rahmen-Quer-Band nMin..nMax, Rahmenfarbe).
|
||||||
|
const push = (from: number, to: number, zBottom: number, zTop: number): void =>
|
||||||
|
pushC(from, to, zBottom, zTop, params.nMin, params.nMax, FRAME_RGB);
|
||||||
// Pfosten (Laibungsseiten), über die volle Öffnungshöhe durchlaufend.
|
// Pfosten (Laibungsseiten), über die volle Öffnungshöhe durchlaufend.
|
||||||
push(iv.from, iv.from + fw, vert.zBottom, vert.zTop);
|
push(iv.from, iv.from + fw, vert.zBottom, vert.zTop);
|
||||||
push(iv.to - fw, iv.to, vert.zBottom, vert.zTop);
|
push(iv.to - fw, iv.to, vert.zBottom, vert.zTop);
|
||||||
@@ -2099,27 +2214,107 @@ function frameMeshesForOpening(
|
|||||||
push(sashFrom, sashTo, center - fw / 2, center + fw / 2);
|
push(sashFrom, sashTo, center - fw / 2, center + fw / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Flügelrahmen je ÖFFENBAREM Flügel: ein Rahmen-Riegel-Ring (links/rechts/
|
}
|
||||||
// oben/unten) INNERHALB des Blendrahmen-Felds, um `reveal` eingerückt
|
// Flügelrahmen je ÖFFENBAREM Flügel: ein eigener Rahmen-Riegel-Ring (links/
|
||||||
// (Schachtelung Blendrahmen → Flügelrahmen → Glas). Feste Flügel bekommen
|
// rechts/oben/unten) INNERHALB des Blendrahmen-Felds, um `reveal` eingerückt
|
||||||
// keinen — so unterscheidet sich fest von öffenbar auch im 3D.
|
// (Schachtelung Blendrahmen → Flügelrahmen → Glas). Anders als bisher sitzt der
|
||||||
if (params.sashes.length > 0 && sashTo - sashFrom > EPS && sashTop - sashBottom > EPS) {
|
// Flügelrahmen in einem EIGENEN, nach aussen VORSTEHENDEN und flacheren Quer-
|
||||||
const segs = resolveSashSpans(params.sashes, sashFrom, sashTo);
|
// Band (`sashBackN..sashFrontN`) — dadurch liest er sich als abgesetzter,
|
||||||
const reveal = fw * 0.5;
|
// verschachtelter Körper VOR dem Blendrahmen (VW „fein"), nicht mehr flach mit
|
||||||
const sfw = fw * 0.7;
|
// ihm verschmolzen. Zusätzlich (nur „fein") die DIN-Öffnungssymbole auf dem
|
||||||
for (const seg of segs) {
|
// Glas. Feste Flügel bekommen weder Flügelrahmen noch Symbol.
|
||||||
const s = seg.sash;
|
if (sashesOn && params.sashes.length > 0 && sashTo - sashFrom > EPS && sashTop - sashBottom > EPS) {
|
||||||
if (s.kind !== "fluegel" || !s.opening || s.opening === "fest") continue;
|
const segs = resolveSashSpans(params.sashes, sashFrom, sashTo);
|
||||||
const cf = seg.from + fw + reveal;
|
const reveal = fw * 0.5;
|
||||||
const ct = seg.to - fw - reveal;
|
const sfw = fw * 0.7;
|
||||||
const bz = sashBottom + reveal;
|
// Verschachteltes Flügelrahmen-Band: ~12 mm vor der Blendrahmen-Vorderkante
|
||||||
const tz = sashTop - reveal;
|
// (nMax = Aussenseite) und nur ~50 mm tief — abgesetzte, plastische Stufe.
|
||||||
if (ct - cf <= EPS || tz - bz <= EPS) continue;
|
const sashFrontN = params.nMax + 0.012;
|
||||||
push(cf, cf + sfw, bz, tz); // linker Flügelholm
|
const sashBackN = Math.max(params.nMin, params.nMax - 0.05);
|
||||||
push(ct - sfw, ct, bz, tz); // rechter Flügelholm
|
// Glas-Vorderkante (s. glassPanesForOpening: n1 = nMax − 0.03); die Symbole
|
||||||
push(cf, ct, tz - sfw, tz); // oberer Flügelriegel
|
// liegen als ~2.5 mm dünne dunkle Prismen KNAPP davor (weiter aussen).
|
||||||
push(cf, ct, bz, bz + sfw); // unterer Flügelriegel
|
const glassFrontN = params.nMax - 0.03;
|
||||||
|
const symBackN = glassFrontN;
|
||||||
|
const symFrontN = glassFrontN + 0.003;
|
||||||
|
const pushSash = (from: number, to: number, zB: number, zT: number): void =>
|
||||||
|
pushC(from, to, zB, zT, sashBackN, sashFrontN, FRAME_RGB);
|
||||||
|
const pushSymbol = (u0: number, h0: number, u1: number, h1: number): void => {
|
||||||
|
const mesh = openingPlaneBox(wall, u0, h0, u1, h1, SYMBOL_HALF_WIDTH, symFrontN, symBackN, SYMBOL_RGB);
|
||||||
|
if (mesh) out.push(mesh);
|
||||||
|
};
|
||||||
|
for (const seg of segs) {
|
||||||
|
const s = seg.sash;
|
||||||
|
if (s.kind !== "fluegel" || !s.opening || s.opening === "fest") continue;
|
||||||
|
const cf = seg.from + fw + reveal;
|
||||||
|
const ct = seg.to - fw - reveal;
|
||||||
|
const bz = sashBottom + reveal;
|
||||||
|
const tz = sashTop - reveal;
|
||||||
|
if (ct - cf <= EPS || tz - bz <= EPS) continue;
|
||||||
|
pushSash(cf, cf + sfw, bz, tz); // linker Flügelholm
|
||||||
|
pushSash(ct - sfw, ct, bz, tz); // rechter Flügelholm
|
||||||
|
pushSash(cf, ct, tz - sfw, tz); // oberer Flügelriegel
|
||||||
|
pushSash(cf, ct, bz, bz + sfw); // unterer Flügelriegel
|
||||||
|
// DIN-Öffnungssymbole (nur „fein") auf dem Glasfeld INNERHALB des Flügel-
|
||||||
|
// rahmens — exakt wie in der 2D-Ansicht (s. toElevation): Dreh = V mit
|
||||||
|
// Spitze am Bandseiten-Mittel des Glasfelds, Kipp = V mit Spitze oben Mitte.
|
||||||
|
if (!fine) continue;
|
||||||
|
const gA = cf + sfw;
|
||||||
|
const gB = ct - sfw;
|
||||||
|
const gbz = bz + sfw;
|
||||||
|
const gtz = tz - sfw;
|
||||||
|
if (gB - gA <= EPS || gtz - gbz <= EPS) continue;
|
||||||
|
const kind = s.opening;
|
||||||
|
if (kind === "dreh" || kind === "drehkipp") {
|
||||||
|
const hingeLeft = (s.hingeSide ?? "left") === "left";
|
||||||
|
const apexU = hingeLeft ? gA : gB;
|
||||||
|
const apexH = (gbz + gtz) / 2;
|
||||||
|
const farU = hingeLeft ? gB : gA;
|
||||||
|
pushSymbol(farU, gbz, apexU, apexH);
|
||||||
|
pushSymbol(farU, gtz, apexU, apexH);
|
||||||
}
|
}
|
||||||
|
if (kind === "kipp" || kind === "drehkipp") {
|
||||||
|
const apexU = (gA + gB) / 2;
|
||||||
|
pushSymbol(gA, gbz, apexU, gtz);
|
||||||
|
pushSymbol(gB, gbz, apexU, gtz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fensterbank (Sims, VW „fein"/„mittel"): Bank-Quader unter der Öffnung,
|
||||||
|
// seitlich ~3 cm überstehend, nach aussen (Wand-Aussenfläche +halfThickness)
|
||||||
|
// auskragend, mit einer zweiten, dünneren Tropfkanten-Stufe an der Aussenkante
|
||||||
|
// darunter. Nur Fenster mit `params.drawSill` (sillBoard ≠ "keine"/"innen").
|
||||||
|
if (detailBodies && params.drawSill) {
|
||||||
|
const ov = 0.03; // seitlicher Überstand je Seite
|
||||||
|
const proj = 0.04; // Auskragung nach aussen (vor die Wandfläche)
|
||||||
|
const under = 0.02; // Einbindung unter den Rahmen (in den Reveal)
|
||||||
|
const bankH = 0.04; // Bankdicke
|
||||||
|
const dripH = 0.02; // Höhe der Tropfkanten-Stufe
|
||||||
|
const dripW = 0.02; // Tiefe der Tropfkante an der Aussenkante
|
||||||
|
const outerN = params.halfThickness; // Wand-Aussenfläche
|
||||||
|
const bankTop = vert.zBottom;
|
||||||
|
const bankBottom = bankTop - bankH;
|
||||||
|
// Bank
|
||||||
|
pushC(iv.from - ov, iv.to + ov, bankBottom, bankTop, outerN - under, outerN + proj, SILL_RGB);
|
||||||
|
// Tropfkante (dünnere Stufe darunter, an der Aussenkante)
|
||||||
|
pushC(iv.from - ov, iv.to + ov, bankBottom - dripH, bankBottom, outerN + proj - dripW, outerN + proj, SILL_RGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tür-Zarge mit Umgriff (VW): die Zarge umgreift die Wand — eine schmale
|
||||||
|
// Bekleidungs-Platte VOR beiden Wandflächen, als Ring (Pfosten links/rechts +
|
||||||
|
// Sturz) rund um die Öffnung, ~1.3 cm vor die Fläche vorstehend. Nur
|
||||||
|
// `frameKind === "zarge"`; Blockrahmen bleibt bündig (params.zargeCladding false).
|
||||||
|
if (detailBodies && params.zargeCladding) {
|
||||||
|
const proud = 0.013; // Vorstand vor die Wandfläche
|
||||||
|
const ext = 0.02; // Überstand auf die Wandfläche (über die Laibungskante hinaus)
|
||||||
|
const casW = fw; // Bekleidungsbreite (= Rahmenprofil)
|
||||||
|
for (const sign of [1, -1] as const) {
|
||||||
|
const faceN = sign * params.halfThickness;
|
||||||
|
const nA = faceN;
|
||||||
|
const nB = faceN + sign * proud;
|
||||||
|
pushC(iv.from - ext, iv.from + casW, vert.zBottom, vert.zTop + ext, nA, nB, FRAME_RGB); // Pfosten links
|
||||||
|
pushC(iv.to - casW, iv.to + ext, vert.zBottom, vert.zTop + ext, nA, nB, FRAME_RGB); // Pfosten rechts
|
||||||
|
pushC(iv.from - ext, iv.to + ext, vert.zTop - casW, vert.zTop + ext, nA, nB, FRAME_RGB); // Sturz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -2295,7 +2490,6 @@ function emitOpeningGlass(project: Project): RMesh[] {
|
|||||||
function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMesh[] {
|
function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMesh[] {
|
||||||
// Grob: keine Rahmen/Sprossen — die Öffnung bleibt Loch + Scheibe (schlank).
|
// Grob: keine Rahmen/Sprossen — die Öffnung bleibt Loch + Scheibe (schlank).
|
||||||
if (detail === "grob") return [];
|
if (detail === "grob") return [];
|
||||||
const fine = detail === "fein";
|
|
||||||
const out: RMesh[] = [];
|
const out: RMesh[] = [];
|
||||||
for (const wall of project.walls) {
|
for (const wall of project.walls) {
|
||||||
for (const op of openingsOfWall(project, wall.id)) {
|
for (const op of openingsOfWall(project, wall.id)) {
|
||||||
@@ -2305,7 +2499,7 @@ function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMes
|
|||||||
if (vert.zTop - vert.zBottom <= EPS) continue;
|
if (vert.zTop - vert.zBottom <= EPS) continue;
|
||||||
const params = resolveOpeningFrame(project, wall, op);
|
const params = resolveOpeningFrame(project, wall, op);
|
||||||
if (!params) continue;
|
if (!params) continue;
|
||||||
out.push(...frameMeshesForOpening(wall, iv, vert, params, fine));
|
out.push(...frameMeshesForOpening(wall, iv, vert, params, detail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user