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)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user