Merge: 3D-Öffnungen auf VW-fein-Niveau
Fenster/Türen im 3D deutlich plastischer (Nutzer-Referenz Vectorworks 'fein'): - Flügelrahmen als eigener, ~12 mm vorstehender Körper (Blendrahmen → Flügelprofil → Glas ablesbar), jetzt ab Detailgrad 'mittel' - DIN-Öffnungssymbole AUF dem Glas (dünne dunkle Prismen, frei orientiert via openingPlaneBox): Dreh/Kipp/Dreh-Kipp wie in der 2D-Ansicht, nur 'fein', nur je nicht-festem Flügel - Fensterbank in 3D: Bank + Tropfkanten-Stufe, seitlich überstehend, aussen auskragend (sillBoard-Gating wie 2D) - Tür-Zarge mit Umgriff: Bekleidungsring vor beiden Wandflächen (nur frameKind 'zarge'; Blockrahmen bleibt bündig) Gating: grob nichts · mittel Rahmen/Flügel/Sims/Zarge · fein + Sprossen + Symbole. +4 Mesh-Tests.
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).
|
||||
const o1 = (sampleProject.openings ?? []).find((o) => o.id === "O1");
|
||||
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 =>
|
||||
projectToModel3d(p, { detail }).meshes.filter(isFrame).length;
|
||||
expect(frameCount("grob")).toBe(0); // keine Rahmen/Sprossen
|
||||
expect(frameCount("mittel")).toBe(4); // 2 Pfosten + Sturz + Schwelle (keine Sprossen/Flügelrahmen)
|
||||
// fein: 4 Basis + 1 Mittelpfosten (wingCount 2) + 2 öffenbare Flügel × 4
|
||||
// Flügelrahmen-Riegel = 4 + 1 + 8 = 13.
|
||||
// mittel: 4 Basis (2 Pfosten + Sturz + Schwelle) + 2 öffenbare Flügel × 4
|
||||
// verschachtelte Flügelrahmen-Riegel = 4 + 8 = 12 (keine Mittelpfosten/Sprossen).
|
||||
expect(frameCount("mittel")).toBe(12);
|
||||
// fein: mittel + 1 Flügel-Mittelpfosten (wingCount 2) = 12 + 1 = 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)", () => {
|
||||
// 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
|
||||
|
||||
+217
-19
@@ -401,6 +401,22 @@ const GLASS_HALF_THICK = 0.01;
|
||||
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];
|
||||
/**
|
||||
* 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. */
|
||||
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. */
|
||||
@@ -1872,6 +1888,62 @@ function openingAxisBox(
|
||||
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]`. */
|
||||
function clampRange(v: number, lo: number, hi: number): number {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
@@ -1967,6 +2039,18 @@ interface OpeningFrameParams {
|
||||
* (Schachtelung Blendrahmen → Flügelrahmen → Glas). Türen: leer.
|
||||
*/
|
||||
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 {
|
||||
@@ -1997,6 +2081,10 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
||||
// Teilverglasung: glazingRatio (Glasanteil oben) bei Glasblatt; sonst voll.
|
||||
glazedFraction: Math.max(0.05, Math.min(1, dt.glazingRatio ?? 1)),
|
||||
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);
|
||||
@@ -2037,6 +2125,10 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
||||
glazingPanes: glazingPanesOf(wt),
|
||||
glazedFraction: 1,
|
||||
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
|
||||
* Flügel-Mittelpfosten (`wingCount−1`, vertikal) und Kämpfer-Zeilen
|
||||
* (`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(
|
||||
wall: Wall,
|
||||
iv: { from: number; to: number },
|
||||
vert: { zBottom: number; zTop: number },
|
||||
params: OpeningFrameParams,
|
||||
fine = true,
|
||||
detail: DetailLevel = "fein",
|
||||
): 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 push = (from: number, to: number, zBottom: number, zTop: number): void => {
|
||||
const mesh = openingAxisBox(wall, from, to, zBottom, zTop, params.nMin, params.nMax, FRAME_RGB);
|
||||
// Generischer Riegel mit frei wählbarem Quer-Band [nA,nB] und Farbe.
|
||||
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);
|
||||
};
|
||||
// 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.
|
||||
push(iv.from, iv.from + fw, vert.zBottom, vert.zTop);
|
||||
push(iv.to - fw, iv.to, vert.zBottom, vert.zTop);
|
||||
@@ -2099,14 +2214,34 @@ function frameMeshesForOpening(
|
||||
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
|
||||
// (Schachtelung Blendrahmen → Flügelrahmen → Glas). Feste Flügel bekommen
|
||||
// keinen — so unterscheidet sich fest von öffenbar auch im 3D.
|
||||
if (params.sashes.length > 0 && sashTo - sashFrom > EPS && sashTop - sashBottom > EPS) {
|
||||
}
|
||||
// Flügelrahmen je ÖFFENBAREM Flügel: ein eigener Rahmen-Riegel-Ring (links/
|
||||
// rechts/oben/unten) INNERHALB des Blendrahmen-Felds, um `reveal` eingerückt
|
||||
// (Schachtelung Blendrahmen → Flügelrahmen → Glas). Anders als bisher sitzt der
|
||||
// Flügelrahmen in einem EIGENEN, nach aussen VORSTEHENDEN und flacheren Quer-
|
||||
// Band (`sashBackN..sashFrontN`) — dadurch liest er sich als abgesetzter,
|
||||
// verschachtelter Körper VOR dem Blendrahmen (VW „fein"), nicht mehr flach mit
|
||||
// ihm verschmolzen. Zusätzlich (nur „fein") die DIN-Öffnungssymbole auf dem
|
||||
// Glas. Feste Flügel bekommen weder Flügelrahmen noch Symbol.
|
||||
if (sashesOn && params.sashes.length > 0 && sashTo - sashFrom > EPS && sashTop - sashBottom > EPS) {
|
||||
const segs = resolveSashSpans(params.sashes, sashFrom, sashTo);
|
||||
const reveal = fw * 0.5;
|
||||
const sfw = fw * 0.7;
|
||||
// Verschachteltes Flügelrahmen-Band: ~12 mm vor der Blendrahmen-Vorderkante
|
||||
// (nMax = Aussenseite) und nur ~50 mm tief — abgesetzte, plastische Stufe.
|
||||
const sashFrontN = params.nMax + 0.012;
|
||||
const sashBackN = Math.max(params.nMin, params.nMax - 0.05);
|
||||
// Glas-Vorderkante (s. glassPanesForOpening: n1 = nMax − 0.03); die Symbole
|
||||
// liegen als ~2.5 mm dünne dunkle Prismen KNAPP davor (weiter aussen).
|
||||
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;
|
||||
@@ -2115,11 +2250,71 @@ function frameMeshesForOpening(
|
||||
const bz = sashBottom + reveal;
|
||||
const tz = sashTop - reveal;
|
||||
if (ct - cf <= EPS || tz - bz <= EPS) continue;
|
||||
push(cf, cf + sfw, bz, tz); // linker Flügelholm
|
||||
push(ct - sfw, ct, bz, tz); // rechter Flügelholm
|
||||
push(cf, ct, tz - sfw, tz); // oberer Flügelriegel
|
||||
push(cf, ct, bz, bz + sfw); // unterer Flügelriegel
|
||||
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;
|
||||
@@ -2295,7 +2490,6 @@ function emitOpeningGlass(project: Project): RMesh[] {
|
||||
function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMesh[] {
|
||||
// Grob: keine Rahmen/Sprossen — die Öffnung bleibt Loch + Scheibe (schlank).
|
||||
if (detail === "grob") return [];
|
||||
const fine = detail === "fein";
|
||||
const out: RMesh[] = [];
|
||||
for (const wall of project.walls) {
|
||||
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;
|
||||
const params = resolveOpeningFrame(project, wall, op);
|
||||
if (!params) continue;
|
||||
out.push(...frameMeshesForOpening(wall, iv, vert, params, fine));
|
||||
out.push(...frameMeshesForOpening(wall, iv, vert, params, detail));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -2412,11 +2606,15 @@ function computeJoinsByFloor(project: Project): Map<string, WallCuts> {
|
||||
export interface Model3dOptions {
|
||||
layeredWalls?: boolean;
|
||||
/**
|
||||
* Detailgrad der Öffnungs-Darstellung im 3D (wie im 2D-Grundriss):
|
||||
* Detailgrad der Öffnungs-Darstellung im 3D (wie im 2D-Grundriss, Referenz ist
|
||||
* der VW-Detailgrad "fein"):
|
||||
* • "grob" — keine Rahmen/Sprossen (nur Loch + Scheibe).
|
||||
* • "mittel"— Rahmen (Pfosten/Sturz/Schwelle) + ggf. Kämpfer, keine Sprossen.
|
||||
* • "fein" — zusätzlich Flügel-Mittelpfosten + Kämpfer-Zeilen (Sprossen).
|
||||
* Fehlt es, gilt "fein" (= bisheriges Verhalten, volle Details).
|
||||
* • "mittel"— Blendrahmen (Pfosten/Sturz/Schwelle) + ggf. Kämpfer, dazu die
|
||||
* verschachtelten Flügelrahmen, Fensterbank (Sims) und Zargen-Umgriff — aber
|
||||
* KEINE Sprossen und KEINE DIN-Öffnungssymbole.
|
||||
* • "fein" — zusätzlich Flügel-Mittelpfosten + Kämpfer-Zeilen (Sprossen) und
|
||||
* die DIN-Öffnungssymbole (Dreh/Kipp) auf dem Glas.
|
||||
* Fehlt es, gilt "fein" (= volle Details).
|
||||
*/
|
||||
detail?: DetailLevel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user