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,109 @@
|
||||
// Kontext-Objekt-Auswahl: deriveSelection liefert für ein selektiertes
|
||||
// Kontext-Objekt (importiertes Mesh/Terrain/Konturen) Bbox + reale LV95-
|
||||
// Koordinate (falls project.geoAnchor gesetzt) — s. Nutzer-Wunsch, direkt im
|
||||
// Object-Info-Panel prüfen zu können, ob/wohin georeferenziert importiert wurde.
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { deriveSelection } from "./selectionInfo";
|
||||
import type { Project } from "../model/types";
|
||||
|
||||
const baseProject = {
|
||||
id: "p",
|
||||
name: "T",
|
||||
layers: [],
|
||||
drawingLevels: [
|
||||
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, baseElevation: 0 },
|
||||
],
|
||||
walls: [],
|
||||
drawings2d: [],
|
||||
};
|
||||
|
||||
describe("deriveSelection — Kontext-Objekt", () => {
|
||||
it("importedMesh: Bbox aus x/y (Höhe z ignoriert), ohne geoAnchor kein lv95Center", () => {
|
||||
const project = {
|
||||
...baseProject,
|
||||
context: [
|
||||
{
|
||||
id: "CTX1",
|
||||
type: "importedMesh",
|
||||
name: "Test",
|
||||
positions: [0, 0, 5, 10, 6, 8, 4, 3, 2],
|
||||
indices: [0, 1, 2],
|
||||
},
|
||||
],
|
||||
} as unknown as Project;
|
||||
const sel = deriveSelection(project, [], null, null, null, null, null, null, null, null, "CTX1");
|
||||
expect(sel?.kind).toBe("contextObject");
|
||||
expect(sel?.id).toBe("CTX1");
|
||||
expect(sel?.bbox).toEqual({ minX: 0, minY: 0, maxX: 10, maxY: 6 });
|
||||
expect(sel?.contextObject?.objType).toBe("importedMesh");
|
||||
expect(sel?.contextObject?.lv95Center).toBeUndefined();
|
||||
});
|
||||
|
||||
it("mit geoAnchor: lv95Center = Bbox-Mittelpunkt zurückgerechnet auf LV95", () => {
|
||||
const project = {
|
||||
...baseProject,
|
||||
geoAnchor: { lv95: { e: 2600000, n: 1200000 }, model: { x: 0, y: 0 } },
|
||||
context: [
|
||||
{
|
||||
id: "CTX1",
|
||||
type: "importedMesh",
|
||||
name: "Test",
|
||||
positions: [0, 0, 0, 100, 50, 0],
|
||||
indices: [0, 1, 0],
|
||||
},
|
||||
],
|
||||
} as unknown as Project;
|
||||
const sel = deriveSelection(project, [], null, null, null, null, null, null, null, null, "CTX1");
|
||||
// Bbox-Mittelpunkt (50,25) -> LV95 = origin + Mittelpunkt (origin = lv95 - model = lv95, model (0,0)).
|
||||
expect(sel?.contextObject?.lv95Center).toEqual({ e: 2600050, n: 1200025 });
|
||||
});
|
||||
|
||||
it("mit geoAnchor.model != (0,0): Origin korrekt berücksichtigt", () => {
|
||||
const project = {
|
||||
...baseProject,
|
||||
geoAnchor: { lv95: { e: 2600000, n: 1200000 }, model: { x: 500, y: 300 } },
|
||||
context: [
|
||||
{
|
||||
id: "CTX1",
|
||||
type: "importedMesh",
|
||||
name: "Test",
|
||||
positions: [500, 300, 0],
|
||||
indices: [0, 0, 0],
|
||||
},
|
||||
],
|
||||
} as unknown as Project;
|
||||
const sel = deriveSelection(project, [], null, null, null, null, null, null, null, null, "CTX1");
|
||||
// Der Modellpunkt (500,300) IST der Anker -> muss exakt auf lv95 (2600000,1200000) zurückrechnen.
|
||||
expect(sel?.contextObject?.lv95Center).toEqual({ e: 2600000, n: 1200000 });
|
||||
});
|
||||
|
||||
it("contourSet: Bbox aus contours[].pts statt positions", () => {
|
||||
const project = {
|
||||
...baseProject,
|
||||
context: [
|
||||
{
|
||||
id: "CS1",
|
||||
type: "contourSet",
|
||||
name: "Höhenlinien",
|
||||
contours: [
|
||||
{ z: 400, pts: [{ x: -5, y: 2 }, { x: 8, y: -3 }], closed: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as Project;
|
||||
const sel = deriveSelection(project, [], null, null, null, null, null, null, null, null, "CS1");
|
||||
expect(sel?.contextObject?.objType).toBe("contourSet");
|
||||
expect(sel?.bbox).toEqual({ minX: -5, minY: -3, maxX: 8, maxY: 2 });
|
||||
});
|
||||
|
||||
it("unbekannte Id -> null", () => {
|
||||
const project = { ...baseProject, context: [] } as unknown as Project;
|
||||
expect(deriveSelection(project, [], null, null, null, null, null, null, null, null, "does-not-exist")).toBeNull();
|
||||
});
|
||||
|
||||
it("keine Kontext-Auswahl (null) -> fällt durch zu anderen Kanälen (hier auch leer -> null)", () => {
|
||||
const project = { ...baseProject, context: [] } as unknown as Project;
|
||||
expect(deriveSelection(project, [], null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
// laufen. Die Slice tippt daher nur ihre eigenen Felder; `get()` liefert
|
||||
// RootState (siehe appStore.ts), wir greifen punktuell auf View-Felder zu.
|
||||
|
||||
import { sampleProject } from "../model/sampleProject";
|
||||
import { emptyProject } from "../model/sampleProject";
|
||||
import {
|
||||
flattenCategories,
|
||||
recomputeFloorElevations,
|
||||
@@ -363,7 +363,10 @@ export function createProjectSlice(
|
||||
return {
|
||||
...history,
|
||||
|
||||
project: sampleProject,
|
||||
// TEMPORÄR für manuellen Test (Nutzer-Wunsch): leeres Projekt statt Demo-
|
||||
// Haus. NICHT committen — vor dem nächsten echten Commit zurück auf
|
||||
// `sampleProject` stellen.
|
||||
project: emptyProject,
|
||||
|
||||
setProject,
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
Ceiling,
|
||||
Column,
|
||||
ColumnProfile,
|
||||
ContextObject,
|
||||
Drawing2D,
|
||||
Drawing2DGeom,
|
||||
ExtrudedSolid,
|
||||
@@ -41,6 +42,7 @@ import type {
|
||||
Wall,
|
||||
WallReferenceLine,
|
||||
} from "../model/types";
|
||||
import { localToLv95 } from "../io/lv95";
|
||||
import { columnVerticalExtent } from "../model/wall";
|
||||
import { columnFootprint } from "../geometry/column";
|
||||
import { evaluateRoom, polygonArea } from "../geometry/roomArea";
|
||||
@@ -258,6 +260,8 @@ export interface StairInfo {
|
||||
treadDepth: number;
|
||||
/** Laufrichtung aufwärts (Auf-/Abpfeil-Richtung). */
|
||||
up: boolean;
|
||||
/** Pfeil-Darstellung (offener Winkel- vs. gefüllter Vollpfeil). Fehlt → "line". */
|
||||
arrowStyle?: "line" | "filled";
|
||||
/** Referenzpunkt der Laufbreite (links/mitte/rechts). Fehlt → "mitte". */
|
||||
referenz?: "links" | "mitte" | "rechts";
|
||||
/** Zugewiesener Treppentyp (Bibliothek, `Stair.typeId`) oder undefined. */
|
||||
@@ -348,7 +352,8 @@ export interface Selection {
|
||||
| "door"
|
||||
| "extrudedSolid"
|
||||
| "column"
|
||||
| "roof";
|
||||
| "roof"
|
||||
| "contextObject";
|
||||
id: string;
|
||||
/** Grafik-Kategorie (Ebene) des Elements. */
|
||||
categoryCode: string;
|
||||
@@ -424,6 +429,20 @@ export interface Selection {
|
||||
extrudedSolid?: ExtrudedSolidInfo;
|
||||
/** Stützen-Attribute (nur bei `kind === "column"`). */
|
||||
column?: ColumnInfo;
|
||||
/** Kontext-Objekt-Attribute (nur bei `kind === "contextObject"`). */
|
||||
contextObject?: ContextObjectInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontext-Objekt-spezifische Attribute (importiertes Mesh/Terrain/Konturen-
|
||||
* Satz) für das Object-Info-Panel — v. a. zum Prüfen, OB/WOHIN georeferenziert
|
||||
* importiert wurde (Nutzer-Wunsch: reale Koordinaten direkt sichtbar).
|
||||
*/
|
||||
export interface ContextObjectInfo {
|
||||
objType: ContextObject["type"];
|
||||
/** Reale LV95-Koordinate des Bbox-Mittelpunkts, falls `Project.geoAnchor`
|
||||
* gesetzt ist (sonst `undefined` — kein Bezug bekannt). */
|
||||
lv95Center?: { e: number; n: number };
|
||||
}
|
||||
|
||||
const WALL_FALLBACK_MM = 0.18;
|
||||
@@ -765,6 +784,7 @@ function stairSelection(project: Project, s: Stair): Selection {
|
||||
riserHeight: geo.riserHeight,
|
||||
treadDepth: geo.treadDepth,
|
||||
up: s.up !== false,
|
||||
arrowStyle: s.arrowStyle,
|
||||
referenz: s.referenz,
|
||||
typeId: s.typeId,
|
||||
floorId: s.floorId,
|
||||
@@ -908,6 +928,53 @@ function columnSelection(project: Project, col: Column): Selection {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selektion für ein Kontext-Objekt (importiertes Mesh/Terrain/Konturen-Satz)
|
||||
* ableiten. Bbox aus den rohen x/y-Koordinaten (Meshes: flaches [x,y,z…]-
|
||||
* Array, nur x/y je Vertex; Konturen: `pts` je Kontur) — die Höhe (z) bleibt
|
||||
* unberücksichtigt (2D-Bbox wie bei allen anderen Elementarten). `lv95Center`
|
||||
* nur, wenn `project.geoAnchor` gesetzt ist (sonst kein Rückrechnungs-Bezug).
|
||||
*/
|
||||
function contextObjectSelection(project: Project, obj: ContextObject): Selection {
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
const consider = (x: number, y: number) => {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
};
|
||||
if (obj.type === "contourSet") {
|
||||
for (const c of obj.contours) for (const p of c.pts) consider(p.x, p.y);
|
||||
} else if (obj.type === "image") {
|
||||
consider(obj.min.x, obj.min.y);
|
||||
consider(obj.max.x, obj.max.y);
|
||||
} else {
|
||||
const pos = obj.positions;
|
||||
for (let i = 0; i + 2 < pos.length; i += 3) consider(pos[i], pos[i + 1]);
|
||||
}
|
||||
if (!isFinite(minX)) minX = minY = maxX = maxY = 0;
|
||||
const anchor = project.geoAnchor;
|
||||
const lv95Center = anchor
|
||||
? localToLv95(
|
||||
(minX + maxX) / 2,
|
||||
(minY + maxY) / 2,
|
||||
{ e: anchor.lv95.e - anchor.model.x, n: anchor.lv95.n - anchor.model.y },
|
||||
)
|
||||
: undefined;
|
||||
return {
|
||||
kind: "contextObject",
|
||||
id: obj.id,
|
||||
categoryCode: "",
|
||||
color: "#888888",
|
||||
weightMm: WALL_FALLBACK_MM,
|
||||
bbox: { minX, minY, maxX, maxY },
|
||||
contextObject: { objType: obj.type, lv95Center },
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für ein 2D-Zeichenelement ableiten (gleiche Auflösung wie generatePlan). */
|
||||
function drawingSelection(project: Project, d: Drawing2D): Selection {
|
||||
const ls = d.lineStyleId
|
||||
@@ -963,7 +1030,12 @@ export function deriveSelection(
|
||||
selectedExtrudedSolidId: string | null = null,
|
||||
selectedColumnId: string | null = null,
|
||||
selectedRoofId: string | null = null,
|
||||
selectedContextObjectId: string | null = null,
|
||||
): Selection | null {
|
||||
if (selectedContextObjectId) {
|
||||
const o = (project.context ?? []).find((x) => x.id === selectedContextObjectId);
|
||||
if (o) return contextObjectSelection(project, o);
|
||||
}
|
||||
if (selectedDrawingId) {
|
||||
const d = project.drawings2d.find((x) => x.id === selectedDrawingId);
|
||||
return d ? drawingSelection(project, d) : null;
|
||||
|
||||
@@ -22,6 +22,9 @@ export interface SiteSlice {
|
||||
addContextObjects: (objs: ContextObject[]) => void;
|
||||
/** Entfernt ein Kontext-Objekt per ID (No-op bei unbekannter ID). */
|
||||
removeContextObject: (id: string) => void;
|
||||
/** Entfernt mehrere Kontext-Objekte in EINEM Schritt (z. B. eine ganze
|
||||
* Ebene/Gruppe importierter Gebäude) — ein einziger Undo-Eintrag. */
|
||||
removeContextObjects: (ids: string[]) => void;
|
||||
/**
|
||||
* Erzeugt aus einem ContourSet (per ID) ein Gelände-TIN und fügt es als
|
||||
* weiteres Kontext-Objekt hinzu. Liefert die ID des neuen TerrainMesh, oder
|
||||
@@ -64,6 +67,12 @@ export function createSiteSlice(
|
||||
removeContextObject: (id) =>
|
||||
setContext((ctx) => ctx.filter((o) => o.id !== id)),
|
||||
|
||||
removeContextObjects: (ids) => {
|
||||
if (ids.length === 0) return;
|
||||
const drop = new Set(ids);
|
||||
setContext((ctx) => ctx.filter((o) => !drop.has(o.id)));
|
||||
},
|
||||
|
||||
generateTerrain: (contourSetId) => {
|
||||
const ctx = get().project.context ?? [];
|
||||
const set = ctx.find(
|
||||
|
||||
Reference in New Issue
Block a user