2D-Plan-Qualitaet: saubere Wandecken + glatte Dämmschraffur
Wandecken (generatePlan/glPlanCompile/PlanView): die diagonale Miter-Stirnkante am L-Stoss wurde als Umriss gestrokt -> Barb-Ueberstand am Aussenapex + 45deg- Naht zwischen gleichen Schichten. Fix: interne Join-Stirnkanten per neuem noStrokeEdges vom Umriss ausnehmen (Fuellung bleibt volle Flaeche, laengs laufende Materialfugen bleiben). Ecke = sauberer Miter ohne Ueberstand, gleiche Schichten verschmelzen nahtlos ueber die Ecke. GPU- und SVG-Pfad teilen sich noStrokeEdges. Dämmschraffur (glPlanHatch): die Sinuswelle wurde pro Segment einzeln aufs Polygon geclippt und mit Butt-Caps gestrokt -> fransige Fragmente an jeder Wellenbiegung. Fix: kontinuierliche Punkt-Laeufe (clipPolylineToPolygon) als EIN gehrter Streifen; Dash laeuft ueber die Laeufe. STEPS 12->22. Gerade Scharen (diagonal/crosshatch) unveraendert.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Unit-Tests für die Wandecken-Gehrung im Grundriss (generatePlan):
|
||||
* • KEINE 45°-Naht zwischen gleichfarbigen Schichten zweier Wände am Knoten
|
||||
* (die inneren Gehrungs-Stirnkanten tragen `noStrokeEdges`).
|
||||
* • KEIN überschießender Umriss-Barb: die Gehrungs-Diagonale ist der Umriss
|
||||
* nicht mehr gestrichen; die Cut-Ecken sitzen exakt auf den Fläche×Fläche-
|
||||
* Apexen (sauberer Miter, kein Overshoot).
|
||||
* • Freie (rechtwinklige) Wandenden bleiben voll umrissen (kein noStrokeEdges).
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { generatePlan } from "./generatePlan";
|
||||
import type { Project, Wall } from "../model/types";
|
||||
|
||||
/** Minimalprojekt: zwei Wände, die in (5,0) eine 90°-L-Ecke bilden. */
|
||||
function cornerProject(): Project {
|
||||
const wall = (id: string, start: Wall["start"], end: Wall["end"]): Wall => ({
|
||||
id,
|
||||
type: "wall",
|
||||
floorId: "eg",
|
||||
categoryCode: "20",
|
||||
start,
|
||||
end,
|
||||
wallTypeId: "aw",
|
||||
height: 2.6,
|
||||
});
|
||||
return {
|
||||
id: "t",
|
||||
name: "T",
|
||||
lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }],
|
||||
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
|
||||
components: [
|
||||
{ id: "a", name: "A", color: "#d8d2c7", hatchId: "none", joinPriority: 10 },
|
||||
{ id: "b", name: "B", color: "#8c5544", hatchId: "none", joinPriority: 50 },
|
||||
],
|
||||
wallTypes: [
|
||||
{
|
||||
id: "aw",
|
||||
name: "AW",
|
||||
layers: [
|
||||
{ componentId: "a", thickness: 0.1 },
|
||||
{ componentId: "b", thickness: 0.245 },
|
||||
],
|
||||
},
|
||||
],
|
||||
drawingLevels: [
|
||||
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
|
||||
],
|
||||
layers: [{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
||||
walls: [
|
||||
// W1 endet in (5,0); W2 startet in (5,0) → geteilter Knoten (L-Ecke).
|
||||
wall("W1", { x: 0, y: 0 }, { x: 5, y: 0 }),
|
||||
wall("W2", { x: 5, y: 0 }, { x: 5, y: 4 }),
|
||||
],
|
||||
doors: [],
|
||||
openings: [],
|
||||
ceilings: [],
|
||||
stairs: [],
|
||||
rooms: [],
|
||||
drawings2d: [],
|
||||
context: [],
|
||||
};
|
||||
}
|
||||
|
||||
const visible = new Set(["20"]);
|
||||
|
||||
describe("generatePlan — Wandecken-Gehrung", () => {
|
||||
it("markiert die Gehrungs-Stirnkante am Knoten als nicht zu stricheln (keine Naht/Barbe)", () => {
|
||||
const plan = generatePlan(cornerProject(), "eg", visible, undefined, "mittel");
|
||||
const wallPolys = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId != null,
|
||||
);
|
||||
expect(wallPolys.length).toBeGreaterThan(0);
|
||||
// JEDES Wandpolygon (Schichtbänder + Umriss) trägt an genau EINEM Ende die
|
||||
// Gehrungs-Stirnkante: W1 am Endcut (Kante 1), W2 am Startcut (Kante 3).
|
||||
const w1 = wallPolys.filter((p) => p.wallId === "W1");
|
||||
const w2 = wallPolys.filter((p) => p.wallId === "W2");
|
||||
expect(w1.length).toBeGreaterThan(0);
|
||||
expect(w2.length).toBeGreaterThan(0);
|
||||
for (const p of w1) expect(p.noStrokeEdges).toEqual([1]);
|
||||
for (const p of w2) expect(p.noStrokeEdges).toEqual([3]);
|
||||
});
|
||||
|
||||
it("legt die Umriss-Cut-Ecken exakt auf die Fläche×Fläche-Apexe (sauberer Miter, kein Overshoot)", () => {
|
||||
const plan = generatePlan(cornerProject(), "eg", visible, undefined, "mittel");
|
||||
// Der Wand-Umriss ist das letzte Wandpolygon je Wand (fill:"none").
|
||||
const outlineW1 = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId === "W1" && p.fill === "none",
|
||||
);
|
||||
expect(outlineW1.length).toBe(1);
|
||||
const o = outlineW1[0];
|
||||
// clippedBand-Reihenfolge [A.start, A.end, B.end, B.start]; A.end/B.end sind
|
||||
// die getrimmten Enden am Knoten. T=0.345, half=0.1725. Aussen-Apex=(5.1725,
|
||||
// -0.1725), Innen-Apex=(4.8275, 0.1725) — der EXAKTE 90°-Miter ohne Überstand.
|
||||
const half = 0.345 / 2;
|
||||
expect(o.pts[1].x).toBeCloseTo(5 + half, 4); // A.end (Aussenfläche)
|
||||
expect(o.pts[1].y).toBeCloseTo(-half, 4);
|
||||
expect(o.pts[2].x).toBeCloseTo(5 - half, 4); // B.end (Innenfläche)
|
||||
expect(o.pts[2].y).toBeCloseTo(half, 4);
|
||||
});
|
||||
|
||||
it("lässt freie (rechtwinklige) Wandenden voll umrissen (kein noStrokeEdges dort)", () => {
|
||||
// Nur W1 allein → beide Enden frei, kein Knoten.
|
||||
const proj = cornerProject();
|
||||
proj.walls = [proj.walls[0]];
|
||||
const plan = generatePlan(proj, "eg", visible, undefined, "mittel");
|
||||
const wallPolys = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId === "W1",
|
||||
);
|
||||
expect(wallPolys.length).toBeGreaterThan(0);
|
||||
for (const p of wallPolys) expect(p.noStrokeEdges).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user