Fenster/Tür: 2D und 3D setzen "aussen" jetzt auf dieselbe Wandfläche

Nutzer-Report: "im 2D innen bündig und im 3D aussen bündig". Root Cause: das
2D-Vorzeichen für insetFace:"aussen" (windowSymbol) war gegenüber der echten
Aussenschicht der Wand (erste Wandtyp-Schicht, kleinster Achs-Offset in
addWallPoche/pushSegment) invertiert — "aussen" landete am inneren
Wandputz statt am äusseren. Dieselbe Vorzeichenverwechslung steckte auch in
addOpeningFrameBand (Tür-Rahmenband), den Sturzlinien und dem Rollladenkasten.
Die 3D-Seite (openingAxisBox/resolveFrameNormalRange) war bereits korrekt
(zwei kompensierende Vorzeichen ergaben zufällig das richtige Ergebnis) und
bleibt unverändert.

+1 Regressionstest, der 2D- und 3D-Rahmenposition direkt gegen die bekannte
Aussenschicht der Wand prüft.
This commit is contained in:
2026-07-12 21:24:48 +02:00
parent cc74396854
commit a631ddaace
6 changed files with 145 additions and 31 deletions
+67 -1
View File
@@ -9,7 +9,7 @@ import { describe, it, expect } from "vitest";
import type { Project, Roof, Wall } from "../model/types";
import { sampleProject } from "../model/sampleProject";
import { selectionHighlightLines, projectToWalls3d, projectToModel3d, pickGeometry } from "./toWalls3d";
import { resolveHatch } from "./generatePlan";
import { resolveHatch, generatePlan } from "./generatePlan";
import { roofGeometry, roofBaseElevation } from "../geometry/roof";
const RGB: [number, number, number] = [1, 0.5, 0.1];
@@ -852,6 +852,72 @@ describe("Rahmen-/Sprossen-Riegel typisierter Öffnungen (emitOpeningFrames)", (
expect(insetMinY - flushMinY).toBeCloseTo(0.1, 6);
});
it("2D (windowSymbol) und 3D (emitOpeningFrames) legen die 'aussen'-Rahmenkante auf DIESELBE Wandfläche — und die stimmt mit der echten Aussenschicht der Wand überein (Regression: 'im 2D innen bündig, im 3D aussen bündig')", () => {
// W1 (u=(1,0), Wandtyp "aw", Gesamtdicke 0.345 m, Achse mittig) -> die
// echte Aussenfläche (erste Schicht, render-ext) liegt bei WELT-y = -0.1725
// exakt (unabhängig von diesem Bug herleitbar: halbe Wanddicke). Ein
// bündiges Fenster (insetFace:"aussen", insetFromFace:0) muss in BEIDEN
// Ansichten exakt dort ansetzen.
const flushWindow: Project = {
...sampleProject,
windowTypes: [
...(sampleProject.windowTypes ?? []),
{
id: "win-consistency-test",
name: "Test bündig aussen",
kind: "fest",
wingCount: 1,
glazing: "einfach",
frameThickness: 0.06,
frameWidth: 0.06,
insetFromFace: 0,
insetFace: "aussen",
defaultSillHeight: 0.9,
defaultWidth: 1.0,
defaultHeight: 1.2,
},
],
openings: [
{
id: "OC1",
type: "opening",
hostWallId: "W1",
categoryCode: "21",
kind: "window",
typeId: "win-consistency-test",
position: 3.0,
width: 1.0,
height: 1.2,
sillHeight: 0.9,
},
],
doors: [],
context: [],
};
const trueAussenY = -0.345 / 2;
// 3D: minimale y-Koordinate aller Rahmen-Meshes dieser Öffnung.
const isFrame3d = (m: { color: [number, number, number] }): boolean =>
m.color[0] === 0.85 && m.color[1] === 0.85 && m.color[2] === 0.83;
let min3d = Infinity;
for (const mesh of projectToModel3d(flushWindow).meshes.filter(isFrame3d)) {
for (let i = 1; i < mesh.positions.length; i += 3) min3d = Math.min(min3d, mesh.positions[i]);
}
expect(min3d).toBeCloseTo(trueAussenY, 6);
// 2D: minimale y-Koordinate des Blendrahmen-Polygons (windowSymbol.frame,
// gerendert als "polygon" mit openingId — die unsichtbare Pick-Fläche hat
// stroke:"none" und wird ausgeschlossen).
const visible = new Set(["20", "21"]);
const plan = generatePlan(flushWindow, "eg", visible, undefined, "mittel");
const framePoly = plan.primitives.find(
(pr) => pr.kind === "polygon" && pr.openingId === "OC1" && pr.stroke !== "none",
);
expect(framePoly).toBeDefined();
const min2d = Math.min(...(framePoly!.kind === "polygon" ? framePoly!.pts.map((p) => p.y) : []));
expect(min2d).toBeCloseTo(trueAussenY, 6);
});
it("Öffnung ohne auflösbaren Typ -> KEIN Rahmen-Mesh (Rückwärtskompatibilität)", () => {
const p: Project = {
...sampleProject,