Files
DOSSIER-STANDALONE/src/plan/generatePlan.context.test.ts
T
karim 68a0459d0e Schnittebenen 2D↔3D Phase 1+2, native Fenster, Hell/Dunkel-Theme, SWISSIMAGE-Import
- Schnittebenen: 3D-Live-Schnitt folgt der gewählten Grundriss-Schnittlinie
  (section3dCutId/sectionPlaneFromLevel), Schalter "Im 3D schneiden" im
  Objekt-Info, Doppelklick auf Schnittlinie springt in 2D-Schnittansicht,
  unsichtbare Ebenen blenden ihre Führungslinie aus
- Eigene native Tauri-Fenster für Kontext-Import/Zeichnungsebenen/
  Ebenen-Einstellungen/Ressourcen/Einstellungen + klassische Menüleiste
  (AppMenuBar) neben der Wortmarke
- Hell/Dunkel-Umschalter (Einstellungen → Darstellung), persistiert,
  flackerfrei vor erstem Render gesetzt
- SWISSIMAGE-Luftbild-Import (swisstopo WMS) als Kontext-Hintergrundebene
- UI-Politur: Werkzeug-Panel Symbole/Liste umschaltbar, Topbar-Quick-Access-
  Icons entfernt, Zahnrad→Einstellungen in Panel-Köpfen, Footerbar/
  Snap-Marker/Maß-HUD auf helle Pillen-Sprache umgestellt
- Neues Dachziegel-Material (RoofingTiles013A)
2026-07-20 10:51:37 +02:00

207 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Kontext-Schicht im Grundriss (generatePlan → addContextContours):
// • ImportedMesh (Gebäude) -> Sockelkanten-Fussabdruck (context-line).
// • TerrainMesh -> nur 2D-Bounding-Box-Umriss (4 Linien).
// • Nutzer-Report: importierte Gebäude waren im Plan UNSICHTBAR (nur 3D).
import { describe, it, expect } from "vitest";
import { generatePlan } from "./generatePlan";
import type { Project } from "../model/types";
function projectWith(context: Project["context"]): Project {
return {
id: "t",
name: "T",
lineStyles: [],
hatches: [],
components: [],
wallTypes: [],
drawingLevels: [
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
],
layers: [],
walls: [],
doors: [],
openings: [],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context,
} as unknown as Project;
}
const visible = new Set<string>();
function contextLines(p: Project) {
return generatePlan(p, "eg", visible).primitives.filter(
(pr) => pr.kind === "line" && (pr as { cls?: string }).cls === "context-line",
);
}
describe("generatePlan — Kontext-Fussabdruck (ImportedMesh)", () => {
it("zeichnet die Sockelkanten eines importierten Gebäudes als context-line", () => {
// Ein simpler Quader-„Bau": Grundfläche 0..2 × 0..3 bei z=0 (Sockel), Dach
// bei z=5. Zwei Sockel-Dreiecke bilden das Boden-Rechteck.
const p = projectWith([
{
id: "B1",
type: "importedMesh",
name: "Haus",
positions: [
0, 0, 0, 2, 0, 0, 2, 3, 0, 0, 3, 0, // 0..3: Boden (z=0)
1, 1, 5, // 4: Dachspitze (z=5, über der Toleranz)
],
indices: [
0, 1, 2, 0, 2, 3, // Boden (Sockel)
0, 1, 4, // Dachdreieck (nur eine Kante 0-1 am Sockel, 4 ist hoch)
],
},
]);
const lines = contextLines(p);
// Sockelkanten: Rechteck-Umriss 0-1,1-2,2-3,3-0 + Diagonale 0-2 = 5 eindeutige.
// Die hohen Dach-Kanten (0-4, 1-4) fallen weg (z=5 > Toleranz).
expect(lines.length).toBe(5);
// Keine Linie berührt die Dachspitze (1,1) auf z-Höhe — aber XY (1,1) könnte
// von der Diagonale getroffen werden; hier prüfen wir, dass keine Kante zu
// einem Punkt führt, dessen Original-z über der Toleranz lag: alle Endpunkte
// liegen im Boden-Rechteck [0..2]×[0..3].
for (const l of lines) {
if (l.kind !== "line") continue;
for (const pt of [l.a, l.b]) {
expect(pt.x).toBeGreaterThanOrEqual(0);
expect(pt.x).toBeLessThanOrEqual(2);
expect(pt.y).toBeGreaterThanOrEqual(0);
expect(pt.y).toBeLessThanOrEqual(3);
}
}
});
it("entdoppelt geteilte Kanten (kein doppeltes Zeichnen)", () => {
// Zwei Sockel-Dreiecke teilen sich die Kante 0-2 -> darf nur EINMAL erscheinen.
const p = projectWith([
{
id: "B1",
type: "importedMesh",
name: "Haus",
positions: [0, 0, 0, 2, 0, 0, 2, 3, 0, 0, 3, 0],
indices: [0, 1, 2, 0, 2, 3],
},
]);
const lines = contextLines(p);
// 4 Rand + 1 geteilte Diagonale = 5 (nicht 6).
expect(lines.length).toBe(5);
});
});
describe("generatePlan — Kontext-Objekte anklickbar (Pick-Region)", () => {
it("importedMesh bekommt ein unsichtbares Bbox-Pick-Band mit contextObjectId", () => {
const p = projectWith([
{
id: "B1",
type: "importedMesh",
name: "Haus",
positions: [0, 0, 0, 2, 0, 0, 2, 3, 0, 0, 3, 0],
indices: [0, 1, 2, 0, 2, 3],
},
]);
const plan = generatePlan(p, "eg", visible);
const pick = plan.primitives.find(
(pr) => pr.kind === "polygon" && pr.contextObjectId === "B1",
);
expect(pick).toBeDefined();
if (pick?.kind === "polygon") {
expect(pick.fill).toBe("none");
expect(pick.stroke).toBe("none");
// Bbox der Grundfläche: [0..2]×[0..3].
const xs = pick.pts.map((p) => p.x);
const ys = pick.pts.map((p) => p.y);
expect(Math.min(...xs)).toBe(0);
expect(Math.max(...xs)).toBe(2);
expect(Math.min(...ys)).toBe(0);
expect(Math.max(...ys)).toBe(3);
}
});
it("terrainMesh bekommt ebenfalls ein Pick-Band mit contextObjectId", () => {
const p = projectWith([
{
id: "T1",
type: "terrainMesh",
name: "Gelände",
positions: [0, 0, 400, 10, 0, 401, 5, 8, 402],
indices: [0, 1, 2],
},
]);
const plan = generatePlan(p, "eg", visible);
const pick = plan.primitives.find(
(pr) => pr.kind === "polygon" && pr.contextObjectId === "T1",
);
expect(pick).toBeDefined();
});
});
describe("generatePlan — Kontext-Umriss (TerrainMesh)", () => {
it("zeichnet nur die 2D-Bounding-Box (4 Linien), nicht das ganze TIN", () => {
// 3 Vertices spannen ein Dreieck; die Bbox-Ausdehnung ist [0..10]×[0..8].
const p = projectWith([
{
id: "T1",
type: "terrainMesh",
name: "Gelände",
positions: [0, 0, 400, 10, 0, 401, 5, 8, 402],
indices: [0, 1, 2],
},
]);
const lines = contextLines(p);
expect(lines.length).toBe(4);
const xs = lines.flatMap((l) => (l.kind === "line" ? [l.a.x, l.b.x] : []));
const ys = lines.flatMap((l) => (l.kind === "line" ? [l.a.y, l.b.y] : []));
expect(Math.min(...xs)).toBe(0);
expect(Math.max(...xs)).toBe(10);
expect(Math.min(...ys)).toBe(0);
expect(Math.max(...ys)).toBe(8);
});
});
describe("generatePlan — Luftbild (ContextImage)", () => {
it("gibt georeferenzierte Bilder als backgroundImages aus (nicht als Primitive)", () => {
const p = projectWith([
{
id: "IMG1",
type: "image",
name: "Luftbild",
layer: "Luftbild",
src: "data:image/jpeg;base64,AAAA",
min: { x: -100, y: -80 },
max: { x: 100, y: 80 },
},
]);
const plan = generatePlan(p, "eg", visible);
expect(plan.backgroundImages).toHaveLength(1);
expect(plan.backgroundImages![0]).toMatchObject({
id: "IMG1",
src: "data:image/jpeg;base64,AAAA",
min: { x: -100, y: -80 },
max: { x: 100, y: 80 },
opacity: 1,
});
// Kein Bild-Primitiv in den Vektor-Primitiven (DXF/PDF lassen es aus).
expect(plan.primitives.some((pr) => (pr as { kind?: string }).kind === "image")).toBe(false);
});
it("im Mono-Modus wird das Luftbild ausgeblendet (reiner S/W-Plan)", () => {
const p = projectWith([
{
id: "IMG1",
type: "image",
name: "Luftbild",
src: "data:image/jpeg;base64,AAAA",
min: { x: 0, y: 0 },
max: { x: 10, y: 10 },
},
]);
const plan = generatePlan(p, "eg", visible, undefined, "mittel", false, true);
expect(plan.backgroundImages).toHaveLength(0);
});
});