Dächer anwählbar + editierbar + löschbar
Platzierte Dächer sind jetzt vollwertige Auswahl-Elemente (analog Decke): - 2D-Auswahl per Klick: unsichtbares Traufe-Pick-Polygon (roofId) + pickRoof (Punkt-in-Polygon, niedrigste Priorität) in PlanView. - Auswahl-Kanal selectedRoofIds (selectionSlice), updateRoof (projectSlice), RoofInfo + roofSelection + deriveSelection-Dispatch (selectionInfo). - Attribut-Panel „Dach": Form (Sattel/Walm/Pult/Mansarde/Zelt/Flach), Firstrichtung X/Y, Neigung(en), Überstand, Dachdicke — live editierbar über host.onSetRoofPatch; dazu abgeleitete Firsthöhe + Grundfläche (read-only). - Löschen (Delete) + Abwählen bei Geschosswechsel/Marquee/anderer Auswahl. +2 Tests (Roof-Selection). tsc + vitest 642 + vite build grün.
This commit is contained in:
@@ -235,6 +235,12 @@ export interface ProjectSlice {
|
||||
patch: Partial<import("../model/types").Opening>,
|
||||
) => void;
|
||||
|
||||
/** Immutable Änderung eines Dachs (Form/Neigung/Überstand/Firstrichtung/Dicke). */
|
||||
updateRoof: (
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Roof>,
|
||||
) => void;
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────────
|
||||
/** Verschiebt eine Treppe (Antritt) um `delta`. */
|
||||
moveStairBy: (stairId: string, delta: Vec2) => void;
|
||||
@@ -913,6 +919,13 @@ export function createProjectSlice(
|
||||
openings: (p.openings ?? []).map((o) => (o.id === id ? { ...o, ...patch } : o)),
|
||||
})),
|
||||
|
||||
// ── Dach-Editieren ─────────────────────────────────────────────────────
|
||||
updateRoof: (id, patch) =>
|
||||
setProject((p) => ({
|
||||
...p,
|
||||
roofs: (p.roofs ?? []).map((r) => (r.id === id ? { ...r, ...patch } : r)),
|
||||
})),
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────
|
||||
// (coalesceKey: siehe Kommentar bei moveGripOf.)
|
||||
moveStairBy: (stairId, delta) =>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Dach-Auswahl: deriveSelection liefert für ein selektiertes Dach eine
|
||||
// Roof-Selection mit den erwarteten Attributen (Form/Neigung/Firsthöhe/Fläche).
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { deriveSelection } from "./selectionInfo";
|
||||
import type { Project, Roof } from "../model/types";
|
||||
|
||||
const roof: Roof = {
|
||||
id: "RF1",
|
||||
type: "roof",
|
||||
floorId: "eg",
|
||||
categoryCode: "31",
|
||||
outline: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 6, y: 0 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: 0, y: 4 },
|
||||
],
|
||||
shape: "sattel",
|
||||
pitchDeg: 45,
|
||||
overhang: 0,
|
||||
ridgeAxis: "x",
|
||||
thickness: 0.2,
|
||||
};
|
||||
|
||||
const project = {
|
||||
id: "p",
|
||||
name: "T",
|
||||
layers: [{ code: "31", name: "Dächer", color: "#7a4a3a", lw: 0.35, visible: true, locked: false }],
|
||||
drawingLevels: [
|
||||
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, baseElevation: 0 },
|
||||
],
|
||||
walls: [],
|
||||
drawings2d: [],
|
||||
roofs: [roof],
|
||||
} as unknown as Project;
|
||||
|
||||
describe("deriveSelection — Dach", () => {
|
||||
it("liefert eine Roof-Selection mit Form + abgeleiteten Werten", () => {
|
||||
const sel = deriveSelection(project, [], null, null, null, null, null, null, null, "RF1");
|
||||
expect(sel?.kind).toBe("roof");
|
||||
expect(sel?.roof?.shape).toBe("sattel");
|
||||
expect(sel?.roof?.pitchDeg).toBe(45);
|
||||
// Firsthöhe = halbe Tiefe (2) · tan45 (1) = 2.
|
||||
expect(sel?.roof?.ridgeHeight).toBeCloseTo(2, 6);
|
||||
// Grundfläche 6×4 = 24.
|
||||
expect(sel?.roof?.footprintArea).toBeCloseTo(24, 6);
|
||||
expect(sel?.id).toBe("RF1");
|
||||
});
|
||||
|
||||
it("ohne Dach-Auswahl kein Roof (null bei leerer Auswahl)", () => {
|
||||
expect(deriveSelection(project, [], null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,8 @@ import type {
|
||||
LayerCategory,
|
||||
Opening,
|
||||
Project,
|
||||
Roof,
|
||||
RoofShape,
|
||||
Room,
|
||||
SiaCategory,
|
||||
SliceTermination,
|
||||
@@ -49,6 +51,7 @@ import {
|
||||
wallVerticalExtent,
|
||||
} from "../model/wall";
|
||||
import { ceilingArea, outlineBBox } from "../geometry/ceiling";
|
||||
import { roofBBox, roofBaseElevation, roofGeometry } from "../geometry/roof";
|
||||
import { openingGapQuad, openingVerticalExtent } from "../geometry/opening";
|
||||
import { stairGeometry, stairBBox } from "../geometry/stair";
|
||||
|
||||
@@ -70,6 +73,25 @@ export interface FloorChoice {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Dach-Attribute fürs Objekt-Info-Panel (nur bei Auswahl genau eines Dachs). */
|
||||
export interface RoofInfo {
|
||||
shape: RoofShape;
|
||||
/** Hauptneigung (Grad). */
|
||||
pitchDeg: number;
|
||||
/** Obere Neigung (Grad) — nur Mansarde relevant. */
|
||||
pitchUpperDeg?: number;
|
||||
/** Dachüberstand an der Traufe (Meter). */
|
||||
overhang: number;
|
||||
/** Firstrichtung entlang X oder Y. */
|
||||
ridgeAxis: "x" | "y";
|
||||
/** Dachdicke (Meter). */
|
||||
thickness: number;
|
||||
/** Firsthöhe über der Traufe (Meter, abgeleitet). */
|
||||
ridgeHeight: number;
|
||||
/** Grundfläche des Umriss-Rechtecks (m², ohne Überstand). */
|
||||
footprintArea: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine wählbare Schichttrennlinie (Fuge zwischen zwei Schichten) als
|
||||
* Referenzlinie einer mehrschichtigen Wand. `offset` ist der fertige
|
||||
@@ -299,7 +321,8 @@ export interface Selection {
|
||||
| "drawing2d"
|
||||
| "door"
|
||||
| "extrudedSolid"
|
||||
| "column";
|
||||
| "column"
|
||||
| "roof";
|
||||
id: string;
|
||||
/** Grafik-Kategorie (Ebene) des Elements. */
|
||||
categoryCode: string;
|
||||
@@ -357,6 +380,8 @@ export interface Selection {
|
||||
wall?: WallInfo;
|
||||
/** Decken-Attribute (nur bei `kind === "ceiling"`). */
|
||||
ceiling?: CeilingInfo;
|
||||
/** Dach-Attribute (nur bei `kind === "roof"`). */
|
||||
roof?: RoofInfo;
|
||||
/** Öffnungs-Attribute (nur bei `kind === "opening"`). */
|
||||
opening?: OpeningInfo;
|
||||
/** Treppen-Attribute (nur bei `kind === "stair"`). */
|
||||
@@ -577,6 +602,37 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für ein Dach ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||
function roofSelection(project: Project, roof: Roof): Selection {
|
||||
const cat = findCategory(project.layers, roof.categoryCode);
|
||||
const color = roof.color ?? cat?.color ?? WALL_FALLBACK_COLOR;
|
||||
const weightMm = cat?.lw ?? WALL_FALLBACK_MM;
|
||||
const bb = roofBBox(roof.outline);
|
||||
const eavesZ = roofBaseElevation(project, roof);
|
||||
const g = roofGeometry(roof, eavesZ);
|
||||
const footprintArea = Math.abs(bb.x1 - bb.x0) * Math.abs(bb.y1 - bb.y0);
|
||||
const roofInfo: RoofInfo = {
|
||||
shape: roof.shape,
|
||||
pitchDeg: roof.pitchDeg,
|
||||
pitchUpperDeg: roof.pitchUpperDeg,
|
||||
overhang: roof.overhang,
|
||||
ridgeAxis: roof.ridgeAxis,
|
||||
thickness: roof.thickness,
|
||||
ridgeHeight: g.ridgeHeight,
|
||||
footprintArea,
|
||||
};
|
||||
return {
|
||||
kind: "roof",
|
||||
id: roof.id,
|
||||
categoryCode: roof.categoryCode,
|
||||
color,
|
||||
weightMm,
|
||||
closed: true,
|
||||
bbox: { minX: bb.x0, minY: bb.y0, maxX: bb.x1, maxY: bb.y1 },
|
||||
roof: roofInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für eine Öffnung ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||
function openingSelection(project: Project, o: Opening): Selection {
|
||||
const cat = findCategory(project.layers, o.categoryCode);
|
||||
@@ -847,6 +903,7 @@ export function deriveSelection(
|
||||
selectedRoomId: string | null = null,
|
||||
selectedExtrudedSolidId: string | null = null,
|
||||
selectedColumnId: string | null = null,
|
||||
selectedRoofId: string | null = null,
|
||||
): Selection | null {
|
||||
if (selectedDrawingId) {
|
||||
const d = project.drawings2d.find((x) => x.id === selectedDrawingId);
|
||||
@@ -873,6 +930,10 @@ export function deriveSelection(
|
||||
const c = (project.ceilings ?? []).find((x) => x.id === selectedCeilingId);
|
||||
return c ? ceilingSelection(project, c) : null;
|
||||
}
|
||||
if (selectedRoofId) {
|
||||
const r = (project.roofs ?? []).find((x) => x.id === selectedRoofId);
|
||||
return r ? roofSelection(project, r) : null;
|
||||
}
|
||||
if (selectedStairId) {
|
||||
const s = (project.stairs ?? []).find((x) => x.id === selectedStairId);
|
||||
return s ? stairSelection(project, s) : null;
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface SelectionSlice {
|
||||
selectedExtrudedSolidIds: string[];
|
||||
// Gewählte Stützen (Tragwerk) als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||
selectedColumnIds: string[];
|
||||
// Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||
selectedRoofIds: string[];
|
||||
|
||||
setSelectedWallIds: (ids: string[]) => void;
|
||||
setSelectedDrawingIds: (ids: string[]) => void;
|
||||
@@ -32,7 +34,8 @@ export interface SelectionSlice {
|
||||
setSelectedRoomIds: (ids: string[]) => void;
|
||||
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
||||
setSelectedColumnIds: (ids: string[]) => void;
|
||||
/** Auswahl (Wände + Decken + Öffnungen + Treppen + 2D-Elemente) leeren. */
|
||||
setSelectedRoofIds: (ids: string[]) => void;
|
||||
/** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */
|
||||
clearSelection: () => void;
|
||||
}
|
||||
|
||||
@@ -49,6 +52,7 @@ export function createSelectionSlice(
|
||||
selectedRoomIds: [],
|
||||
selectedExtrudedSolidIds: [],
|
||||
selectedColumnIds: [],
|
||||
selectedRoofIds: [],
|
||||
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
||||
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
||||
setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }),
|
||||
@@ -57,6 +61,7 @@ export function createSelectionSlice(
|
||||
setSelectedRoomIds: (ids) => set({ selectedRoomIds: ids }),
|
||||
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
|
||||
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
|
||||
setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }),
|
||||
clearSelection: () =>
|
||||
set({
|
||||
selectedWallIds: [],
|
||||
@@ -67,6 +72,7 @@ export function createSelectionSlice(
|
||||
selectedRoomIds: [],
|
||||
selectedExtrudedSolidIds: [],
|
||||
selectedColumnIds: [],
|
||||
selectedRoofIds: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user