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:
+54
-67
@@ -569,6 +569,11 @@ export default function App() {
|
|||||||
const setSelectedCeilingIds = useStore((s) => s.setSelectedCeilingIds);
|
const setSelectedCeilingIds = useStore((s) => s.setSelectedCeilingIds);
|
||||||
const selectedCeilingId =
|
const selectedCeilingId =
|
||||||
selectedCeilingIds.length === 1 ? selectedCeilingIds[0] : null;
|
selectedCeilingIds.length === 1 ? selectedCeilingIds[0] : null;
|
||||||
|
// Dach-Auswahl (MENGE); Einzel-Element bei genau einem gewählten Dach.
|
||||||
|
const selectedRoofIds = useStore((s) => s.selectedRoofIds);
|
||||||
|
const setSelectedRoofIds = useStore((s) => s.setSelectedRoofIds);
|
||||||
|
const selectedRoofId = selectedRoofIds.length === 1 ? selectedRoofIds[0] : null;
|
||||||
|
const updateRoof = useStore((s) => s.updateRoof);
|
||||||
const selectedOpeningIds = useStore((s) => s.selectedOpeningIds);
|
const selectedOpeningIds = useStore((s) => s.selectedOpeningIds);
|
||||||
const setSelectedOpeningIds = useStore((s) => s.setSelectedOpeningIds);
|
const setSelectedOpeningIds = useStore((s) => s.setSelectedOpeningIds);
|
||||||
const selectedOpeningId =
|
const selectedOpeningId =
|
||||||
@@ -618,6 +623,7 @@ export default function App() {
|
|||||||
setSelectedRoomIds([]);
|
setSelectedRoomIds([]);
|
||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedRoofIds([]);
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
setEditor(null);
|
setEditor(null);
|
||||||
}, [activeLevelId]);
|
}, [activeLevelId]);
|
||||||
@@ -1844,7 +1850,8 @@ export default function App() {
|
|||||||
selectedStairIds.length ||
|
selectedStairIds.length ||
|
||||||
selectedRoomIds.length ||
|
selectedRoomIds.length ||
|
||||||
selectedExtrudedSolidIds.length ||
|
selectedExtrudedSolidIds.length ||
|
||||||
selectedColumnIds.length
|
selectedColumnIds.length ||
|
||||||
|
selectedRoofIds.length
|
||||||
) {
|
) {
|
||||||
const dids = new Set(selectedDrawingIds);
|
const dids = new Set(selectedDrawingIds);
|
||||||
const wids = new Set(selectedWallIds);
|
const wids = new Set(selectedWallIds);
|
||||||
@@ -1854,11 +1861,13 @@ export default function App() {
|
|||||||
const rmids = new Set(selectedRoomIds);
|
const rmids = new Set(selectedRoomIds);
|
||||||
const exids = new Set(selectedExtrudedSolidIds);
|
const exids = new Set(selectedExtrudedSolidIds);
|
||||||
const colids = new Set(selectedColumnIds);
|
const colids = new Set(selectedColumnIds);
|
||||||
|
const rfids = new Set(selectedRoofIds);
|
||||||
setProject((p) => ({
|
setProject((p) => ({
|
||||||
...p,
|
...p,
|
||||||
drawings2d: p.drawings2d.filter((d) => !dids.has(d.id)),
|
drawings2d: p.drawings2d.filter((d) => !dids.has(d.id)),
|
||||||
walls: p.walls.filter((w) => !wids.has(w.id)),
|
walls: p.walls.filter((w) => !wids.has(w.id)),
|
||||||
ceilings: (p.ceilings ?? []).filter((c) => !cids.has(c.id)),
|
ceilings: (p.ceilings ?? []).filter((c) => !cids.has(c.id)),
|
||||||
|
roofs: (p.roofs ?? []).filter((r) => !rfids.has(r.id)),
|
||||||
stairs: (p.stairs ?? []).filter((s) => !stids.has(s.id)),
|
stairs: (p.stairs ?? []).filter((s) => !stids.has(s.id)),
|
||||||
rooms: (p.rooms ?? []).filter((r) => !rmids.has(r.id)),
|
rooms: (p.rooms ?? []).filter((r) => !rmids.has(r.id)),
|
||||||
extrudedSolids: (p.extrudedSolids ?? []).filter((s) => !exids.has(s.id)),
|
extrudedSolids: (p.extrudedSolids ?? []).filter((s) => !exids.has(s.id)),
|
||||||
@@ -1877,6 +1886,7 @@ export default function App() {
|
|||||||
setSelectedRoomIds([]);
|
setSelectedRoomIds([]);
|
||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedRoofIds([]);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1892,9 +1902,11 @@ export default function App() {
|
|||||||
selectedStairIds,
|
selectedStairIds,
|
||||||
selectedExtrudedSolidIds,
|
selectedExtrudedSolidIds,
|
||||||
selectedColumnIds,
|
selectedColumnIds,
|
||||||
|
selectedRoofIds,
|
||||||
project.walls,
|
project.walls,
|
||||||
project.drawings2d,
|
project.drawings2d,
|
||||||
project.ceilings,
|
project.ceilings,
|
||||||
|
project.roofs,
|
||||||
project.stairs,
|
project.stairs,
|
||||||
project.extrudedSolids,
|
project.extrudedSolids,
|
||||||
project.columns,
|
project.columns,
|
||||||
@@ -2559,6 +2571,7 @@ export default function App() {
|
|||||||
selectedRoomId,
|
selectedRoomId,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
|
selectedRoofId,
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
project,
|
project,
|
||||||
@@ -2570,6 +2583,7 @@ export default function App() {
|
|||||||
selectedRoomId,
|
selectedRoomId,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
|
selectedRoofId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3116,6 +3130,10 @@ export default function App() {
|
|||||||
setResourcesTab(kind === "door" ? "doorStyles" : "windowStyles");
|
setResourcesTab(kind === "door" ? "doorStyles" : "windowStyles");
|
||||||
setResourcesOpen(true);
|
setResourcesOpen(true);
|
||||||
},
|
},
|
||||||
|
// ── Dach-Attribute (nur bei selektiertem Dach) ─────────────────────────
|
||||||
|
onSetRoofPatch: (patch) => {
|
||||||
|
if (selection?.kind === "roof") updateRoof(selection.id, patch);
|
||||||
|
},
|
||||||
// ── Treppen-Attribute (nur bei selektierter Treppe) ────────────────────
|
// ── Treppen-Attribute (nur bei selektierter Treppe) ────────────────────
|
||||||
onSetStairShape: (shape) => {
|
onSetStairShape: (shape) => {
|
||||||
if (selection?.kind === "stair") updateStair(selection.id, { shape });
|
if (selection?.kind === "stair") updateStair(selection.id, { shape });
|
||||||
@@ -4220,93 +4238,61 @@ export default function App() {
|
|||||||
? selectedColumnIds.filter((x) => x !== id)
|
? selectedColumnIds.filter((x) => x !== id)
|
||||||
: [...selectedColumnIds, id],
|
: [...selectedColumnIds, id],
|
||||||
);
|
);
|
||||||
|
} else if (sel.roofId) {
|
||||||
|
const id = sel.roofId;
|
||||||
|
setSelectedRoofIds(
|
||||||
|
selectedRoofIds.includes(id)
|
||||||
|
? selectedRoofIds.filter((x) => x !== id)
|
||||||
|
: [...selectedRoofIds, id],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Ohne Shift: ersetzen. Öffnung hat Vorrang (ihr Symbol liegt in der Lücke),
|
// Ohne Shift: ersetzen. Öffnung hat Vorrang (ihr Symbol liegt in der Lücke),
|
||||||
// dann Wand, dann 2D-Element, dann Decke; sonst leeren. Die Kanäle schließen
|
// dann Wand, dann 2D-Element, dann Decke; sonst leeren. Die Kanäle schließen
|
||||||
// sich gegenseitig aus.
|
// sich gegenseitig aus.
|
||||||
|
// Alle Kanäle ausser dem gewählten leeren — als Helfer, damit jeder Zweig
|
||||||
|
// (inkl. des neuen Dach-Kanals) konsistent bleibt.
|
||||||
|
const clearOthers = (keep: string) => {
|
||||||
|
if (keep !== "wall") setSelectedWallIds([]);
|
||||||
|
if (keep !== "drawing") setSelectedDrawingId(null);
|
||||||
|
if (keep !== "ceiling") setSelectedCeilingIds([]);
|
||||||
|
if (keep !== "opening") setSelectedOpeningIds([]);
|
||||||
|
if (keep !== "stair") setSelectedStairIds([]);
|
||||||
|
if (keep !== "room") setSelectedRoomIds([]);
|
||||||
|
if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]);
|
||||||
|
if (keep !== "column") setSelectedColumnIds([]);
|
||||||
|
if (keep !== "roof") setSelectedRoofIds([]);
|
||||||
|
};
|
||||||
if (sel.openingId) {
|
if (sel.openingId) {
|
||||||
setSelectedOpeningIds([sel.openingId]);
|
setSelectedOpeningIds([sel.openingId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("opening");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.wallId) {
|
} else if (sel.wallId) {
|
||||||
setSelectedWallIds([sel.wallId]);
|
setSelectedWallIds([sel.wallId]);
|
||||||
setSelectedDrawingId(null);
|
clearOthers("wall");
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.columnId) {
|
} else if (sel.columnId) {
|
||||||
setSelectedColumnIds([sel.columnId]);
|
setSelectedColumnIds([sel.columnId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("column");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
} else if (sel.drawingId) {
|
} else if (sel.drawingId) {
|
||||||
setSelectedDrawingId(sel.drawingId);
|
setSelectedDrawingId(sel.drawingId);
|
||||||
setSelectedWallIds([]);
|
clearOthers("drawing");
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.extrudedSolidId) {
|
} else if (sel.extrudedSolidId) {
|
||||||
setSelectedExtrudedSolidIds([sel.extrudedSolidId]);
|
setSelectedExtrudedSolidIds([sel.extrudedSolidId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("extrudedSolid");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.ceilingId) {
|
} else if (sel.ceilingId) {
|
||||||
setSelectedCeilingIds([sel.ceilingId]);
|
setSelectedCeilingIds([sel.ceilingId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("ceiling");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.stairId) {
|
} else if (sel.stairId) {
|
||||||
setSelectedStairIds([sel.stairId]);
|
setSelectedStairIds([sel.stairId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("stair");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else if (sel.roomId) {
|
} else if (sel.roomId) {
|
||||||
setSelectedRoomIds([sel.roomId]);
|
setSelectedRoomIds([sel.roomId]);
|
||||||
setSelectedWallIds([]);
|
clearOthers("room");
|
||||||
setSelectedDrawingId(null);
|
} else if (sel.roofId) {
|
||||||
setSelectedCeilingIds([]);
|
setSelectedRoofIds([sel.roofId]);
|
||||||
setSelectedOpeningIds([]);
|
clearOthers("roof");
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedWallIds([]);
|
clearOthers("");
|
||||||
setSelectedDrawingId(null);
|
|
||||||
setSelectedCeilingIds([]);
|
|
||||||
setSelectedOpeningIds([]);
|
|
||||||
setSelectedStairIds([]);
|
|
||||||
setSelectedRoomIds([]);
|
|
||||||
setSelectedExtrudedSolidIds([]);
|
|
||||||
setSelectedColumnIds([]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Links-Drag (Marquee) im Grundriss: die getroffene Wand-MENGE übernehmen
|
// Links-Drag (Marquee) im Grundriss: die getroffene Wand-MENGE übernehmen
|
||||||
@@ -4314,14 +4300,15 @@ export default function App() {
|
|||||||
const onPlanMarquee = (sel: MarqueeSelection) => {
|
const onPlanMarquee = (sel: MarqueeSelection) => {
|
||||||
setSelectedWallIds(sel.wallIds);
|
setSelectedWallIds(sel.wallIds);
|
||||||
setSelectedDrawingIds(sel.drawingIds ?? []);
|
setSelectedDrawingIds(sel.drawingIds ?? []);
|
||||||
// Decken/Öffnungen/Treppen/Räume/Extrusionen sind (noch) nicht Teil des
|
// Decken/Öffnungen/Treppen/Räume/Extrusionen/Dächer sind (noch) nicht Teil
|
||||||
// Marquee → beim Aufziehen abwählen.
|
// des Marquee → beim Aufziehen abwählen.
|
||||||
setSelectedCeilingIds([]);
|
setSelectedCeilingIds([]);
|
||||||
setSelectedOpeningIds([]);
|
setSelectedOpeningIds([]);
|
||||||
setSelectedStairIds([]);
|
setSelectedStairIds([]);
|
||||||
setSelectedRoomIds([]);
|
setSelectedRoomIds([]);
|
||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedRoofIds([]);
|
||||||
};
|
};
|
||||||
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die
|
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die
|
||||||
// NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die
|
// NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die
|
||||||
|
|||||||
@@ -285,6 +285,25 @@ export const de = {
|
|||||||
"objinfo.kind.room": "Raum",
|
"objinfo.kind.room": "Raum",
|
||||||
"objinfo.kind.extrudedSolid": "Extrusion",
|
"objinfo.kind.extrudedSolid": "Extrusion",
|
||||||
"objinfo.kind.column": "Stütze",
|
"objinfo.kind.column": "Stütze",
|
||||||
|
"objinfo.kind.roof": "Dach",
|
||||||
|
// ── Dach-Attribute (Object-Info-Panel) ──────────────────────────────────
|
||||||
|
"objinfo.roof.section": "Dach",
|
||||||
|
"objinfo.roof.shape": "Dachform",
|
||||||
|
"objinfo.roof.shape.flach": "Flachdach",
|
||||||
|
"objinfo.roof.shape.pult": "Pultdach",
|
||||||
|
"objinfo.roof.shape.sattel": "Satteldach",
|
||||||
|
"objinfo.roof.shape.walm": "Walmdach",
|
||||||
|
"objinfo.roof.shape.mansarde": "Mansarddach",
|
||||||
|
"objinfo.roof.shape.zelt": "Zeltdach",
|
||||||
|
"objinfo.roof.ridgeAxis": "Firstrichtung",
|
||||||
|
"objinfo.roof.ridgeAxis.x": "entlang X",
|
||||||
|
"objinfo.roof.ridgeAxis.y": "entlang Y",
|
||||||
|
"objinfo.roof.pitch": "Neigung (°)",
|
||||||
|
"objinfo.roof.pitchUpper": "Obere Neigung (°)",
|
||||||
|
"objinfo.roof.overhang": "Überstand (m)",
|
||||||
|
"objinfo.roof.thickness": "Dachdicke (m)",
|
||||||
|
"objinfo.roof.ridgeHeight": "Firsthöhe",
|
||||||
|
"objinfo.roof.area": "Grundfläche",
|
||||||
// ── Stützen-Attribute (Object-Info-Panel) ───────────────────────────────
|
// ── Stützen-Attribute (Object-Info-Panel) ───────────────────────────────
|
||||||
"objinfo.column.section": "Stütze",
|
"objinfo.column.section": "Stütze",
|
||||||
"objinfo.column.profile": "Profil",
|
"objinfo.column.profile": "Profil",
|
||||||
|
|||||||
@@ -284,6 +284,25 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"objinfo.kind.room": "Room",
|
"objinfo.kind.room": "Room",
|
||||||
"objinfo.kind.extrudedSolid": "Extrusion",
|
"objinfo.kind.extrudedSolid": "Extrusion",
|
||||||
"objinfo.kind.column": "Column",
|
"objinfo.kind.column": "Column",
|
||||||
|
"objinfo.kind.roof": "Roof",
|
||||||
|
// ── Roof attributes (object info panel) ─────────────────────────────────
|
||||||
|
"objinfo.roof.section": "Roof",
|
||||||
|
"objinfo.roof.shape": "Roof shape",
|
||||||
|
"objinfo.roof.shape.flach": "Flat roof",
|
||||||
|
"objinfo.roof.shape.pult": "Mono-pitch roof",
|
||||||
|
"objinfo.roof.shape.sattel": "Gable roof",
|
||||||
|
"objinfo.roof.shape.walm": "Hip roof",
|
||||||
|
"objinfo.roof.shape.mansarde": "Mansard roof",
|
||||||
|
"objinfo.roof.shape.zelt": "Pyramid roof",
|
||||||
|
"objinfo.roof.ridgeAxis": "Ridge direction",
|
||||||
|
"objinfo.roof.ridgeAxis.x": "along X",
|
||||||
|
"objinfo.roof.ridgeAxis.y": "along Y",
|
||||||
|
"objinfo.roof.pitch": "Pitch (°)",
|
||||||
|
"objinfo.roof.pitchUpper": "Upper pitch (°)",
|
||||||
|
"objinfo.roof.overhang": "Overhang (m)",
|
||||||
|
"objinfo.roof.thickness": "Roof thickness (m)",
|
||||||
|
"objinfo.roof.ridgeHeight": "Ridge height",
|
||||||
|
"objinfo.roof.area": "Footprint area",
|
||||||
// ── Column attributes (object info panel) ────────────────────────────────
|
// ── Column attributes (object info panel) ────────────────────────────────
|
||||||
"objinfo.column.section": "Column",
|
"objinfo.column.section": "Column",
|
||||||
"objinfo.column.profile": "Profile",
|
"objinfo.column.profile": "Profile",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { ColorHexField } from "../ui/ColorHexField";
|
|||||||
import {
|
import {
|
||||||
WallSection,
|
WallSection,
|
||||||
CeilingSection,
|
CeilingSection,
|
||||||
|
RoofSection,
|
||||||
OpeningSection,
|
OpeningSection,
|
||||||
StairSection,
|
StairSection,
|
||||||
RoomSection,
|
RoomSection,
|
||||||
@@ -314,6 +315,7 @@ export function AttributesPanel() {
|
|||||||
Darstellung + die Host-Handler. */}
|
Darstellung + die Host-Handler. */}
|
||||||
{sel.wall && <WallSection wall={sel.wall} host={host} />}
|
{sel.wall && <WallSection wall={sel.wall} host={host} />}
|
||||||
{sel.ceiling && <CeilingSection ceiling={sel.ceiling} host={host} />}
|
{sel.ceiling && <CeilingSection ceiling={sel.ceiling} host={host} />}
|
||||||
|
{sel.roof && <RoofSection roof={sel.roof} host={host} />}
|
||||||
{sel.opening && <OpeningSection opening={sel.opening} host={host} />}
|
{sel.opening && <OpeningSection opening={sel.opening} host={host} />}
|
||||||
{sel.stair && <StairSection stair={sel.stair} host={host} />}
|
{sel.stair && <StairSection stair={sel.stair} host={host} />}
|
||||||
{sel.room && <RoomSection room={sel.room} roomId={sel.id} host={host} />}
|
{sel.room && <RoomSection room={sel.room} roomId={sel.id} host={host} />}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import type { TranslationKey } from "../i18n";
|
||||||
import { formatM } from "../model/types";
|
import { formatM } from "../model/types";
|
||||||
import type {
|
import type {
|
||||||
|
RoofShape,
|
||||||
SiaCategory,
|
SiaCategory,
|
||||||
SliceTermination,
|
SliceTermination,
|
||||||
StairShape,
|
StairShape,
|
||||||
@@ -32,6 +34,7 @@ import type {
|
|||||||
ColumnInfo,
|
ColumnInfo,
|
||||||
ExtrudedSolidInfo,
|
ExtrudedSolidInfo,
|
||||||
OpeningInfo,
|
OpeningInfo,
|
||||||
|
RoofInfo,
|
||||||
RoomInfo,
|
RoomInfo,
|
||||||
StairInfo,
|
StairInfo,
|
||||||
WallInfo,
|
WallInfo,
|
||||||
@@ -431,6 +434,107 @@ export function ColumnSection({
|
|||||||
// ── Treppen-Attribut-Abschnitt ───────────────────────────────────────────────
|
// ── Treppen-Attribut-Abschnitt ───────────────────────────────────────────────
|
||||||
// Grundform (gerade/L/Wendel), Laufbreite, Stufenanzahl, Steighöhe (+ abgeleitete
|
// Grundform (gerade/L/Wendel), Laufbreite, Stufenanzahl, Steighöhe (+ abgeleitete
|
||||||
// Steigungshöhe/Auftrittstiefe), Laufrichtung; Referenzgeschoss + UK/OK read-only.
|
// Steigungshöhe/Auftrittstiefe), Laufrichtung; Referenzgeschoss + UK/OK read-only.
|
||||||
|
/** Dach-Abschnitt: Form, Neigung(en), Überstand, Firstrichtung, Dicke. */
|
||||||
|
export function RoofSection({
|
||||||
|
roof,
|
||||||
|
host,
|
||||||
|
}: {
|
||||||
|
roof: RoofInfo;
|
||||||
|
host: ReturnType<typeof usePanelHost>;
|
||||||
|
}) {
|
||||||
|
const numField = (
|
||||||
|
labelKey: TranslationKey,
|
||||||
|
value: number,
|
||||||
|
step: number,
|
||||||
|
apply: (v: number) => void,
|
||||||
|
) => (
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t(labelKey)}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="objinfo-text"
|
||||||
|
step={step}
|
||||||
|
value={Number(value.toFixed(2))}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = Number(e.target.value);
|
||||||
|
if (Number.isFinite(v)) apply(v);
|
||||||
|
}}
|
||||||
|
title={t(labelKey)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="objinfo-sep" />
|
||||||
|
<div className="objinfo-section-label">{t("objinfo.roof.section")}</div>
|
||||||
|
|
||||||
|
{/* Dachform. */}
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t("objinfo.roof.shape")}</span>
|
||||||
|
<Dropdown
|
||||||
|
value={roof.shape}
|
||||||
|
onChange={(v) => host.onSetRoofPatch({ shape: v as RoofShape })}
|
||||||
|
options={[
|
||||||
|
{ value: "flach", label: t("objinfo.roof.shape.flach") },
|
||||||
|
{ value: "pult", label: t("objinfo.roof.shape.pult") },
|
||||||
|
{ value: "sattel", label: t("objinfo.roof.shape.sattel") },
|
||||||
|
{ value: "walm", label: t("objinfo.roof.shape.walm") },
|
||||||
|
{ value: "mansarde", label: t("objinfo.roof.shape.mansarde") },
|
||||||
|
{ value: "zelt", label: t("objinfo.roof.shape.zelt") },
|
||||||
|
]}
|
||||||
|
title={t("objinfo.roof.shape")}
|
||||||
|
width={130}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Firstrichtung (X/Y) — nur bei Formen mit First relevant. */}
|
||||||
|
{roof.shape !== "flach" && roof.shape !== "zelt" && (
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t("objinfo.roof.ridgeAxis")}</span>
|
||||||
|
<Dropdown
|
||||||
|
value={roof.ridgeAxis}
|
||||||
|
onChange={(v) => host.onSetRoofPatch({ ridgeAxis: v as "x" | "y" })}
|
||||||
|
options={[
|
||||||
|
{ value: "x", label: t("objinfo.roof.ridgeAxis.x") },
|
||||||
|
{ value: "y", label: t("objinfo.roof.ridgeAxis.y") },
|
||||||
|
]}
|
||||||
|
title={t("objinfo.roof.ridgeAxis")}
|
||||||
|
width={130}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Neigung(en) — Flachdach hat keine. */}
|
||||||
|
{roof.shape !== "flach" &&
|
||||||
|
numField("objinfo.roof.pitch", roof.pitchDeg, 1, (v) =>
|
||||||
|
host.onSetRoofPatch({ pitchDeg: Math.max(0, Math.min(85, v)) }),
|
||||||
|
)}
|
||||||
|
{roof.shape === "mansarde" &&
|
||||||
|
numField("objinfo.roof.pitchUpper", roof.pitchUpperDeg ?? roof.pitchDeg / 2, 1, (v) =>
|
||||||
|
host.onSetRoofPatch({ pitchUpperDeg: Math.max(0, Math.min(85, v)) }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Überstand + Dicke (Meter). */}
|
||||||
|
{numField("objinfo.roof.overhang", roof.overhang, 0.05, (v) =>
|
||||||
|
host.onSetRoofPatch({ overhang: Math.max(0, v) }),
|
||||||
|
)}
|
||||||
|
{numField("objinfo.roof.thickness", roof.thickness, 0.02, (v) =>
|
||||||
|
host.onSetRoofPatch({ thickness: Math.max(0.01, v) }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Abgeleitete Werte (read-only). */}
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t("objinfo.roof.ridgeHeight")}</span>
|
||||||
|
<span className="objinfo-fval">{formatM(roof.ridgeHeight)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t("objinfo.roof.area")}</span>
|
||||||
|
<span className="objinfo-fval">{roof.footprintArea.toFixed(2)} m²</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function StairSection({
|
export function StairSection({
|
||||||
stair,
|
stair,
|
||||||
host,
|
host,
|
||||||
|
|||||||
@@ -299,6 +299,11 @@ export interface PanelHostValue {
|
|||||||
* (Discoverability: von der gewählten Öffnung in den Typeditor springen).
|
* (Discoverability: von der gewählten Öffnung in den Typeditor springen).
|
||||||
*/
|
*/
|
||||||
onEditOpeningType: (kind: "door" | "window") => void;
|
onEditOpeningType: (kind: "door" | "window") => void;
|
||||||
|
/**
|
||||||
|
* Immutable Änderung von Dach-Attributen (Form/Neigung/Überstand/Firstrichtung/
|
||||||
|
* Dicke) — wirkt NUR auf das selektierte Dach.
|
||||||
|
*/
|
||||||
|
onSetRoofPatch: (patch: Partial<import("../model/types").Roof>) => void;
|
||||||
|
|
||||||
// ── Treppen-Attribute (Object-Info-Panel; nur die selektierte Treppe) ───
|
// ── Treppen-Attribute (Object-Info-Panel; nur die selektierte Treppe) ───
|
||||||
/** Setzt die Grundform (gerade/L/Wendel). */
|
/** Setzt die Grundform (gerade/L/Wendel). */
|
||||||
|
|||||||
+27
-1
@@ -192,6 +192,11 @@ export interface PlanSelection {
|
|||||||
* traf und KEIN Element höherer Priorität (Öffnung/Wand); sonst `null`.
|
* traf und KEIN Element höherer Priorität (Öffnung/Wand); sonst `null`.
|
||||||
*/
|
*/
|
||||||
columnId: string | null;
|
columnId: string | null;
|
||||||
|
/**
|
||||||
|
* ID des getroffenen Dachs (Traufe-Fläche), wenn der Klick es traf und KEIN
|
||||||
|
* Element höherer Priorität (alle anderen gehen vor); sonst `null`.
|
||||||
|
*/
|
||||||
|
roofId: string | null;
|
||||||
/**
|
/**
|
||||||
* War beim Klick Shift gedrückt? Dann ERWEITERT der Aufrufer (App) die
|
* War beim Klick Shift gedrückt? Dann ERWEITERT der Aufrufer (App) die
|
||||||
* Auswahl um das getroffene Element (bzw. schaltet es ab/zu), statt sie zu
|
* Auswahl um das getroffene Element (bzw. schaltet es ab/zu), statt sie zu
|
||||||
@@ -985,6 +990,20 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Getroffenes Dach: das unsichtbare Traufe-Pick-Polygon per Punkt-in-Polygon
|
||||||
|
// (die sichtbaren Dachlinien allein wären schwer exakt zu treffen).
|
||||||
|
const pickRoof = (clientX: number, clientY: number): string | null => {
|
||||||
|
const v = clientToView(clientX, clientY, view);
|
||||||
|
const m = viewToModel(v.x, v.y);
|
||||||
|
for (let i = plan.primitives.length - 1; i >= 0; i--) {
|
||||||
|
const p = plan.primitives[i];
|
||||||
|
if (p.kind === "polygon" && p.roofId && pointInPolygon(m, p.pts)) {
|
||||||
|
return p.roofId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Getroffener extrudierter Körper (truck-Integration): der Footprint-Umriss
|
// Getroffener extrudierter Körper (truck-Integration): der Footprint-Umriss
|
||||||
// ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon
|
// ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon
|
||||||
// trotzdem als FLÄCHE anklickbar (analog Raum, nicht nur auf der Linie).
|
// trotzdem als FLÄCHE anklickbar (analog Raum, nicht nur auf der Linie).
|
||||||
@@ -1536,11 +1555,17 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId
|
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId
|
||||||
? null
|
? null
|
||||||
: pickStair(e.clientX, e.clientY);
|
: pickStair(e.clientX, e.clientY);
|
||||||
// Raum als letzte Priorität: die Raum-Fläche liegt UNTER allem anderen.
|
// Raum als vorletzte Priorität: die Raum-Fläche liegt UNTER allem anderen.
|
||||||
const roomId =
|
const roomId =
|
||||||
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId || stairId
|
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId || stairId
|
||||||
? null
|
? null
|
||||||
: pickRoom(e.clientX, e.clientY);
|
: pickRoom(e.clientX, e.clientY);
|
||||||
|
// Dach als LETZTE Priorität: die Traufe-Fläche deckt den ganzen Grundriss
|
||||||
|
// ab, daher erst wählen, wenn wirklich nichts anderes getroffen wurde.
|
||||||
|
const roofId =
|
||||||
|
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId || stairId || roomId
|
||||||
|
? null
|
||||||
|
: pickRoof(e.clientX, e.clientY);
|
||||||
onSelect({
|
onSelect({
|
||||||
model: viewToModel(v.x, v.y),
|
model: viewToModel(v.x, v.y),
|
||||||
hitIndex,
|
hitIndex,
|
||||||
@@ -1552,6 +1577,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
roomId,
|
roomId,
|
||||||
extrudedSolidId,
|
extrudedSolidId,
|
||||||
columnId,
|
columnId,
|
||||||
|
roofId,
|
||||||
shift: e.shiftKey,
|
shift: e.shiftKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,8 @@ export type Primitive =
|
|||||||
* (Poché) einer Stütze ist (für die Links-Klick-Auswahl der Fläche).
|
* (Poché) einer Stütze ist (für die Links-Klick-Auswahl der Fläche).
|
||||||
*/
|
*/
|
||||||
columnId?: string;
|
columnId?: string;
|
||||||
|
/** ID des Dachs (Roof), falls dieses (unsichtbare) Polygon der Traufe-Umriss ist (Auswahl). */
|
||||||
|
roofId?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
kind: "line";
|
kind: "line";
|
||||||
@@ -2466,6 +2468,23 @@ function addRoof(
|
|||||||
const stroke = roof.color ?? category?.color ?? POCHE_STROKE;
|
const stroke = roof.color ?? category?.color ?? POCHE_STROKE;
|
||||||
const geo = roofGeometry(roof, 0);
|
const geo = roofGeometry(roof, 0);
|
||||||
|
|
||||||
|
// Unsichtbares Pick-Polygon über der Traufe-Fläche — macht das Dach über die
|
||||||
|
// ganze Fläche per Links-Klick selektierbar (die Dachlinien allein wären
|
||||||
|
// schwer exakt zu treffen). fill/stroke "none" ⇒ zeichnet nichts; PlanView
|
||||||
|
// testet nur die `pts` (Punkt-in-Polygon), analog Öffnung/Decke.
|
||||||
|
if (geo.eaves.length >= 3) {
|
||||||
|
out.push({
|
||||||
|
kind: "polygon",
|
||||||
|
pts: geo.eaves,
|
||||||
|
fill: "none",
|
||||||
|
stroke: "none",
|
||||||
|
strokeWidthMm: 0,
|
||||||
|
hatch: NO_HATCH,
|
||||||
|
greyed,
|
||||||
|
roofId: roof.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const n = geo.eaves.length;
|
const n = geo.eaves.length;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
out.push({
|
out.push({
|
||||||
|
|||||||
@@ -235,6 +235,12 @@ export interface ProjectSlice {
|
|||||||
patch: Partial<import("../model/types").Opening>,
|
patch: Partial<import("../model/types").Opening>,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
/** Immutable Änderung eines Dachs (Form/Neigung/Überstand/Firstrichtung/Dicke). */
|
||||||
|
updateRoof: (
|
||||||
|
id: string,
|
||||||
|
patch: Partial<import("../model/types").Roof>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
// ── Treppen-Editieren ──────────────────────────────────────────────────────
|
// ── Treppen-Editieren ──────────────────────────────────────────────────────
|
||||||
/** Verschiebt eine Treppe (Antritt) um `delta`. */
|
/** Verschiebt eine Treppe (Antritt) um `delta`. */
|
||||||
moveStairBy: (stairId: string, delta: Vec2) => void;
|
moveStairBy: (stairId: string, delta: Vec2) => void;
|
||||||
@@ -913,6 +919,13 @@ export function createProjectSlice(
|
|||||||
openings: (p.openings ?? []).map((o) => (o.id === id ? { ...o, ...patch } : o)),
|
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 ──────────────────────────────────────────────────
|
// ── Treppen-Editieren ──────────────────────────────────────────────────
|
||||||
// (coalesceKey: siehe Kommentar bei moveGripOf.)
|
// (coalesceKey: siehe Kommentar bei moveGripOf.)
|
||||||
moveStairBy: (stairId, delta) =>
|
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,
|
LayerCategory,
|
||||||
Opening,
|
Opening,
|
||||||
Project,
|
Project,
|
||||||
|
Roof,
|
||||||
|
RoofShape,
|
||||||
Room,
|
Room,
|
||||||
SiaCategory,
|
SiaCategory,
|
||||||
SliceTermination,
|
SliceTermination,
|
||||||
@@ -49,6 +51,7 @@ import {
|
|||||||
wallVerticalExtent,
|
wallVerticalExtent,
|
||||||
} from "../model/wall";
|
} from "../model/wall";
|
||||||
import { ceilingArea, outlineBBox } from "../geometry/ceiling";
|
import { ceilingArea, outlineBBox } from "../geometry/ceiling";
|
||||||
|
import { roofBBox, roofBaseElevation, roofGeometry } from "../geometry/roof";
|
||||||
import { openingGapQuad, openingVerticalExtent } from "../geometry/opening";
|
import { openingGapQuad, openingVerticalExtent } from "../geometry/opening";
|
||||||
import { stairGeometry, stairBBox } from "../geometry/stair";
|
import { stairGeometry, stairBBox } from "../geometry/stair";
|
||||||
|
|
||||||
@@ -70,6 +73,25 @@ export interface FloorChoice {
|
|||||||
name: string;
|
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
|
* Eine wählbare Schichttrennlinie (Fuge zwischen zwei Schichten) als
|
||||||
* Referenzlinie einer mehrschichtigen Wand. `offset` ist der fertige
|
* Referenzlinie einer mehrschichtigen Wand. `offset` ist der fertige
|
||||||
@@ -299,7 +321,8 @@ export interface Selection {
|
|||||||
| "drawing2d"
|
| "drawing2d"
|
||||||
| "door"
|
| "door"
|
||||||
| "extrudedSolid"
|
| "extrudedSolid"
|
||||||
| "column";
|
| "column"
|
||||||
|
| "roof";
|
||||||
id: string;
|
id: string;
|
||||||
/** Grafik-Kategorie (Ebene) des Elements. */
|
/** Grafik-Kategorie (Ebene) des Elements. */
|
||||||
categoryCode: string;
|
categoryCode: string;
|
||||||
@@ -357,6 +380,8 @@ export interface Selection {
|
|||||||
wall?: WallInfo;
|
wall?: WallInfo;
|
||||||
/** Decken-Attribute (nur bei `kind === "ceiling"`). */
|
/** Decken-Attribute (nur bei `kind === "ceiling"`). */
|
||||||
ceiling?: CeilingInfo;
|
ceiling?: CeilingInfo;
|
||||||
|
/** Dach-Attribute (nur bei `kind === "roof"`). */
|
||||||
|
roof?: RoofInfo;
|
||||||
/** Öffnungs-Attribute (nur bei `kind === "opening"`). */
|
/** Öffnungs-Attribute (nur bei `kind === "opening"`). */
|
||||||
opening?: OpeningInfo;
|
opening?: OpeningInfo;
|
||||||
/** Treppen-Attribute (nur bei `kind === "stair"`). */
|
/** 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). */
|
/** Selektion für eine Öffnung ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||||
function openingSelection(project: Project, o: Opening): Selection {
|
function openingSelection(project: Project, o: Opening): Selection {
|
||||||
const cat = findCategory(project.layers, o.categoryCode);
|
const cat = findCategory(project.layers, o.categoryCode);
|
||||||
@@ -847,6 +903,7 @@ export function deriveSelection(
|
|||||||
selectedRoomId: string | null = null,
|
selectedRoomId: string | null = null,
|
||||||
selectedExtrudedSolidId: string | null = null,
|
selectedExtrudedSolidId: string | null = null,
|
||||||
selectedColumnId: string | null = null,
|
selectedColumnId: string | null = null,
|
||||||
|
selectedRoofId: string | null = null,
|
||||||
): Selection | null {
|
): Selection | null {
|
||||||
if (selectedDrawingId) {
|
if (selectedDrawingId) {
|
||||||
const d = project.drawings2d.find((x) => x.id === 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);
|
const c = (project.ceilings ?? []).find((x) => x.id === selectedCeilingId);
|
||||||
return c ? ceilingSelection(project, c) : null;
|
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) {
|
if (selectedStairId) {
|
||||||
const s = (project.stairs ?? []).find((x) => x.id === selectedStairId);
|
const s = (project.stairs ?? []).find((x) => x.id === selectedStairId);
|
||||||
return s ? stairSelection(project, s) : null;
|
return s ? stairSelection(project, s) : null;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface SelectionSlice {
|
|||||||
selectedExtrudedSolidIds: string[];
|
selectedExtrudedSolidIds: string[];
|
||||||
// Gewählte Stützen (Tragwerk) als MENGE von IDs — getrennt von den übrigen Kanälen.
|
// Gewählte Stützen (Tragwerk) als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||||
selectedColumnIds: string[];
|
selectedColumnIds: string[];
|
||||||
|
// Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||||
|
selectedRoofIds: string[];
|
||||||
|
|
||||||
setSelectedWallIds: (ids: string[]) => void;
|
setSelectedWallIds: (ids: string[]) => void;
|
||||||
setSelectedDrawingIds: (ids: string[]) => void;
|
setSelectedDrawingIds: (ids: string[]) => void;
|
||||||
@@ -32,7 +34,8 @@ export interface SelectionSlice {
|
|||||||
setSelectedRoomIds: (ids: string[]) => void;
|
setSelectedRoomIds: (ids: string[]) => void;
|
||||||
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
||||||
setSelectedColumnIds: (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;
|
clearSelection: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ export function createSelectionSlice(
|
|||||||
selectedRoomIds: [],
|
selectedRoomIds: [],
|
||||||
selectedExtrudedSolidIds: [],
|
selectedExtrudedSolidIds: [],
|
||||||
selectedColumnIds: [],
|
selectedColumnIds: [],
|
||||||
|
selectedRoofIds: [],
|
||||||
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
||||||
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
||||||
setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }),
|
setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }),
|
||||||
@@ -57,6 +61,7 @@ export function createSelectionSlice(
|
|||||||
setSelectedRoomIds: (ids) => set({ selectedRoomIds: ids }),
|
setSelectedRoomIds: (ids) => set({ selectedRoomIds: ids }),
|
||||||
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
|
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
|
||||||
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
|
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
|
||||||
|
setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }),
|
||||||
clearSelection: () =>
|
clearSelection: () =>
|
||||||
set({
|
set({
|
||||||
selectedWallIds: [],
|
selectedWallIds: [],
|
||||||
@@ -67,6 +72,7 @@ export function createSelectionSlice(
|
|||||||
selectedRoomIds: [],
|
selectedRoomIds: [],
|
||||||
selectedExtrudedSolidIds: [],
|
selectedExtrudedSolidIds: [],
|
||||||
selectedColumnIds: [],
|
selectedColumnIds: [],
|
||||||
|
selectedRoofIds: [],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user