Schnitt entspiegelt: u-Achse = Betrachter-RECHTS (Rust + TS koordiniert)

Die Schnitt-u-Achse nutzte cross(normal, up) — die look_at-Konvention für
Blick entlang −z. Für einen Schnitt, den man entlang +normal betrachtet,
spiegelte das die Zeichnung (Blick nach Norden zeigte Osten links). Neu:
u = cross(up, normal) — wer nach Norden blickt, hat Osten rechts. Dieselbe
Entspiegelung wie zuvor in der Ansicht (b967146).

Koordiniert geändert: SectionPlane::u_axis (section.rs; section_fill nutzt
dieselbe Methode) + sectionUAxisModel (toSection.ts; Schicht-Orientierung
wallLayersReversedInU und Dach-Schnitt hängen daran und kippen konsistent
mit — die Aussenseite bleibt in der Zeichnung physisch aussen). Rust- und
TS-Tests auf die neue Konvention gespiegelt; Engine neu gebaut.
cargo 76/76, vitest 737/737.
This commit is contained in:
2026-07-11 02:30:35 +02:00
parent 1945fecd11
commit 983d061a0c
4 changed files with 85 additions and 75 deletions
+20 -16
View File
@@ -66,13 +66,14 @@ const RENDER_EXT_T = 0.02; // Aussenputz — layers[0], die Aussenseite
const RENDER_INT_T = 0.015; // Innenputz — letzte Schicht, die Innenseite
describe("Schnitt-u-Weltachse", () => {
it("entspricht der WASM-Konvention u = cross(normal, up) → Grundriss (Nz, Nx)", () => {
// Schnitt A: Ebenennormale world = (0,0,1) → uWorld model = (1, 0).
it("entspricht der WASM-Konvention u = cross(up, normal) → Grundriss (Nz, Nx)", () => {
// Schnitt A: Ebenennormale world = (0,0,1) → uWorld model = (1, 0)
// Betrachter-RECHTS (Entspiegelung: Blick nach Norden ⇒ Osten rechts).
expect(planeA.normal[0]).toBeCloseTo(0, 9);
expect(planeA.normal[1]).toBeCloseTo(0, 9);
expect(planeA.normal[2]).toBeCloseTo(1, 9);
const u = sectionUAxisModel(planeA);
expect(u.x).toBeCloseTo(-1, 9);
expect(u.x).toBeCloseTo(1, 9);
expect(u.y).toBeCloseTo(0, 9);
});
});
@@ -80,11 +81,14 @@ describe("Schnitt-u-Weltachse", () => {
describe("Wand-Aufbaurichtung vs. Schnitt-u-Achse", () => {
const uWorld = sectionUAxisModel(planeA);
it("W2 (Aussenseite +x) nicht gespiegelt, W4 (Aussenseite x) gespiegelt", () => {
it("W2 (Aussenseite +x) gespiegelt, W4 (Aussenseite x) nicht — entspiegelte u-Achse", () => {
// Beide Wände gehören zum CCW-Umriss; die linke Normale zeigt nach innen.
// Gegenüberliegende Wände ⇒ genau eine der beiden wird gespiegelt.
expect(wallLayersReversedInU(wall("W2"), uWorld)).toBe(false);
expect(wallLayersReversedInU(wall("W4"), uWorld)).toBe(true);
// Gegenüberliegende Wände ⇒ genau eine der beiden wird gespiegelt. (Mit
// der Entspiegelung der u-Achse tauschen die Rollen gegenüber früher —
// Geometrie UND Bandreihenfolge kippen zusammen, die Aussenseite bleibt
// in der Zeichnung physisch aussen.)
expect(wallLayersReversedInU(wall("W2"), uWorld)).toBe(true);
expect(wallLayersReversedInU(wall("W4"), uWorld)).toBe(false);
});
it("degenerierte Wand (Länge 0) bleibt unverändert (kein Spiegeln)", () => {
@@ -97,21 +101,21 @@ describe("splitWallLayers: Aussenputz auf der Aussenseite, Wände spiegelverkehr
const uWorld = sectionUAxisModel(planeA);
const cp = rect(0, 0.345); // volle Wanddicke, Skala 1:1
it("W2: Aussenputz (layers[0]) an uMin, Innenputz an uMax", () => {
it("W2: gespiegelt — Aussenputz an uMax, Innenputz an uMin", () => {
const bands = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W2"), uWorld));
expect(bands).toHaveLength(4);
// uMin-Band = Aussenputz (Breite 0.02), uMax-Band = Innenputz (0.015).
expect(bands[0].width).toBeCloseTo(RENDER_EXT_T, 6);
expect(bands[3].width).toBeCloseTo(RENDER_INT_T, 6);
});
it("W4: gespiegelt — Aussenputz an uMax, Innenputz an uMin", () => {
const bands = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W4"), uWorld));
expect(bands).toHaveLength(4);
// Entspiegelte u-Achse: W2s Aussenseite (+x) liegt jetzt bei uMax.
expect(bands[0].width).toBeCloseTo(RENDER_INT_T, 6);
expect(bands[3].width).toBeCloseTo(RENDER_EXT_T, 6);
});
it("W4: Aussenputz (layers[0]) an uMin, Innenputz an uMax", () => {
const bands = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W4"), uWorld));
expect(bands).toHaveLength(4);
expect(bands[0].width).toBeCloseTo(RENDER_EXT_T, 6);
expect(bands[3].width).toBeCloseTo(RENDER_INT_T, 6);
});
it("propagiert sourceId (Quell-Element) auf ALLE Schicht-Bänder", () => {
const cpWithId: SectionCutPolygon = { ...cp, sourceId: "W2" };
const bands = splitWallLayers(cpWithId, sampleProject, aw, wall("W2"), uWorld);
+12 -10
View File
@@ -49,7 +49,8 @@ function project(r: Roof, extra: Partial<Project> = {}): Project {
} as unknown as Project;
}
/** Schnittebene: Blick entlang +x, Spur = Modell-Gerade x=0 (u wächst mit y). */
/** Schnittebene: Blick entlang +x (Osten), Spur x=0 — Betrachter-RECHTS =
* Süden, u fällt also mit Modell-y (entspiegelte u-Achse). */
const PLANE: SectionPlaneSpec = { point: [0, 0, 0], normal: [1, 0, 0] };
const emptyOutput = (): SectionOutput => ({ cutPolygons: [], visibleEdges: [], hiddenEdges: [] });
@@ -61,10 +62,11 @@ describe("appendRoofSections", () => {
expect(out.cutPolygons.length).toBe(1);
const cp = out.cutPolygons[0];
expect(cp.component).toEqual({ kind: "roof", index: 0 });
// u-Intervall = Tiefe 0..4; Oberkante 5, Unterkante 5 0.3 (cosθ = 1).
// u-Intervall = Tiefe 0..4 → u 4..0 (u = Modell-y); Oberkante 5,
// Unterkante 5 0.3 (cosθ = 1).
const us = cp.pts.map((p) => p[0]).sort((a, b) => a - b);
expect(us[0]).toBeCloseTo(0, 6);
expect(us[3]).toBeCloseTo(4, 6);
expect(us[0]).toBeCloseTo(-4, 6);
expect(us[3]).toBeCloseTo(0, 6);
const vs = cp.pts.map((p) => p[1]).sort((a, b) => a - b);
expect(vs[3]).toBeCloseTo(5, 6);
expect(vs[0]).toBeCloseTo(5 - 0.3, 6);
@@ -76,19 +78,19 @@ describe("appendRoofSections", () => {
expect(out.cutPolygons.length).toBe(2);
const tan30 = Math.tan((30 * Math.PI) / 180);
const cos30 = Math.cos((30 * Math.PI) / 180);
const ridgeZ = 5 + 2 * tan30; // First bei halber Tiefe (y=2)
// Alle Oberkanten-Werte je Band: an u=0/4 Traufe (5), an u=2 First.
const ridgeZ = 5 + 2 * tan30; // First bei halber Tiefe (y=2 → u=2)
// Alle Oberkanten-Werte je Band: an u=0/4 Traufe (5), an u=2 First.
const topAt = (cp: (typeof out.cutPolygons)[number], u: number): number =>
Math.max(...cp.pts.filter((p) => Math.abs(p[0] - u) < 1e-6).map((p) => p[1]));
const bands = out.cutPolygons;
const front = bands.find((b) => b.pts.some((p) => Math.abs(p[0]) < 1e-6));
const back = bands.find((b) => b.pts.some((p) => Math.abs(p[0] - 4) < 1e-6));
const back = bands.find((b) => b.pts.some((p) => Math.abs(p[0] + 4) < 1e-6));
expect(front).toBeDefined();
expect(back).toBeDefined();
expect(topAt(front!, 0)).toBeCloseTo(5, 5);
expect(topAt(front!, 2)).toBeCloseTo(ridgeZ, 5);
expect(topAt(back!, 4)).toBeCloseTo(5, 5);
expect(topAt(back!, 2)).toBeCloseTo(ridgeZ, 5);
expect(topAt(front!, -2)).toBeCloseTo(ridgeZ, 5);
expect(topAt(back!, -4)).toBeCloseTo(5, 5);
expect(topAt(back!, -2)).toBeCloseTo(ridgeZ, 5);
// Vertikale Banddicke = lotrechte Dicke / cos(Neigung).
const vAt = (cp: (typeof out.cutPolygons)[number], u: number): number[] =>
cp.pts.filter((p) => Math.abs(p[0] - u) < 1e-6).map((p) => p[1]);
+7 -6
View File
@@ -160,16 +160,17 @@ export function sectionPlaneFromLevel(level: DrawingLevel): SectionPlaneSpec | n
* Ebene (Modell-2D: model.x = world.x, model.y = world.z).
*
* Muss EXAKT der u-Achse des WASM-Extraktors entsprechen (`SectionPlane::u_axis`
* in section.rs): dort `u = normalize(cross(normal, up))` mit up = (0,1,0). Für
* eine horizontale Normale `[Nx, Ny, Nz]` ist `cross(normal, (0,1,0)) =
* (-Nz, 0, Nx)`; die Grundriss-Komponenten (x, z) sind also `(-Nz, Nx)`. Damit
* wächst die Ausgabe-u-Koordinate genau in dieser Modell-Richtung — die Referenz,
* gegen die die Wand-Aufbaurichtung projiziert wird. (Nicht normiert; für das
* in section.rs): dort `u = normalize(cross(up, normal))` mit up = (0,1,0)
* das Blickrichtungs-RECHTS des Betrachters (Entspiegelung; wer nach Norden
* blickt, hat Osten rechts). Für eine horizontale Normale `[Nx, Ny, Nz]` sind
* die Grundriss-Komponenten (x, z) also `(Nz, -Nx)`. (Nicht normiert; für das
* Vorzeichen des Skalarprodukts irrelevant.)
*/
export function sectionUAxisModel(plane: SectionPlaneSpec): Vec2 {
// Blickrichtungs-RECHTS (cross(up, N)) — muss EXAKT `SectionPlane::u_axis`
// in section.rs spiegeln (dort seit der Entspiegelung dieselbe Formel).
const [nx, , nz] = plane.normal;
return { x: -nz, y: nx };
return { x: nz, y: -nx };
}
/**