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:
2026-07-09 22:18:59 +02:00
parent 89112eff9b
commit 80121a3f9a
12 changed files with 385 additions and 70 deletions
+27 -1
View File
@@ -192,6 +192,11 @@ export interface PlanSelection {
* traf und KEIN Element höherer Priorität (Öffnung/Wand); sonst `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
* 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;
};
// 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
// ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon
// 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
? null
: 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 =
openingId || wallId || columnId || drawingId || extrudedSolidId || ceilingId || stairId
? null
: 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({
model: viewToModel(v.x, v.y),
hitIndex,
@@ -1552,6 +1577,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
roomId,
extrudedSolidId,
columnId,
roofId,
shift: e.shiftKey,
});
}
+19
View File
@@ -258,6 +258,8 @@ export type Primitive =
* (Poché) einer Stütze ist (für die Links-Klick-Auswahl der Fläche).
*/
columnId?: string;
/** ID des Dachs (Roof), falls dieses (unsichtbare) Polygon der Traufe-Umriss ist (Auswahl). */
roofId?: string;
}
| {
kind: "line";
@@ -2466,6 +2468,23 @@ function addRoof(
const stroke = roof.color ?? category?.color ?? POCHE_STROKE;
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;
for (let i = 0; i < n; i++) {
out.push({