Fix: Auswahlrahmen kann jetzt auch Decken/Räume/Treppen anwählen

Das Marquee (Aufziehen eines Auswahl-Rechtecks) las bisher nur wallId/
drawingId aus den Polygon-Primitiven aus — Decken/Räume/Treppen trugen
zwar längst ceilingId/roomId/stairId in denselben Primitiven, wurden aber
nie ausgewertet und beim Aufziehen sogar aktiv abgewählt (war im Code
bereits als bekannte Lücke kommentiert).
This commit is contained in:
2026-08-22 02:43:16 +02:00
parent 467e9580a7
commit cad544ebd7
4 changed files with 124 additions and 12 deletions
+10
View File
@@ -37,6 +37,9 @@ import {
pointSegDistPx, pointSegDistPx,
marqueeHit, marqueeHit,
marqueeHitDrawings, marqueeHitDrawings,
marqueeHitCeilings,
marqueeHitRooms,
marqueeHitStairs,
} from "./planView/geometry"; } from "./planView/geometry";
import { import {
polyUsesPattern, polyUsesPattern,
@@ -208,6 +211,10 @@ export interface MarqueeSelection {
wallIds: string[]; wallIds: string[];
/** Getroffene 2D-Zeichenelemente (gleiche window/crossing-Konvention). */ /** Getroffene 2D-Zeichenelemente (gleiche window/crossing-Konvention). */
drawingIds: string[]; drawingIds: string[];
/** Getroffene Decken/Räume/Treppen (gleiche window/crossing-Konvention). */
ceilingIds: string[];
roomIds: string[];
stairIds: string[];
/** /**
* Zieh-Richtung: `false` = links→rechts (positiv Δx) → nur VOLLSTÄNDIG * Zieh-Richtung: `false` = links→rechts (positiv Δx) → nur VOLLSTÄNDIG
* eingeschlossene Wände; `true` = rechts→links (negativ Δx) → auch berührte/ * eingeschlossene Wände; `true` = rechts→links (negativ Δx) → auch berührte/
@@ -1825,6 +1832,9 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
onMarquee({ onMarquee({
wallIds: marqueeHit(plan.primitives, rect, crossing), wallIds: marqueeHit(plan.primitives, rect, crossing),
drawingIds: marqueeHitDrawings(plan.primitives, rect, crossing), drawingIds: marqueeHitDrawings(plan.primitives, rect, crossing),
ceilingIds: marqueeHitCeilings(plan.primitives, rect, crossing),
roomIds: marqueeHitRooms(plan.primitives, rect, crossing),
stairIds: marqueeHitStairs(plan.primitives, rect, crossing),
crossing, crossing,
}); });
} }
+39 -7
View File
@@ -222,24 +222,56 @@ export function marqueeHit(
rect: ModelRect, rect: ModelRect,
crossing: boolean, crossing: boolean,
): string[] { ): string[] {
// Polygone je Wand sammeln (nur Primitive mit Wandbezug zählen für die Auswahl). return marqueeHitByField(prims, "wallId", rect, crossing);
const byWall = new Map<string, Vec2[][]>(); }
/**
* Generische Variante von {@link marqueeHit} für ein beliebiges Polygon-ID-
* Feld (`ceilingId`/`roomId`/`stairId`/`columnId`/`roofId`/…) — dieselbe
* window/crossing-Konvention, nur auf ein anderes Element bezogen. Deckt
* Nutzer-Report: „Auswahlrahmen kann Decken/Räume/Treppen nicht anwählen"
* (bisher nur Wände über `marqueeHit`, alles andere fehlte komplett).
*/
function marqueeHitByField(
prims: Primitive[],
field: "wallId" | "ceilingId" | "roomId" | "stairId" | "columnId" | "roofId",
rect: ModelRect,
crossing: boolean,
): string[] {
const byId = new Map<string, Vec2[][]>();
for (const p of prims) { for (const p of prims) {
if (p.kind !== "polygon" || p.wallId == null) continue; if (p.kind !== "polygon") continue;
const list = byWall.get(p.wallId); const id = p[field];
if (id == null) continue;
const list = byId.get(id);
if (list) list.push(p.pts); if (list) list.push(p.pts);
else byWall.set(p.wallId, [p.pts]); else byId.set(id, [p.pts]);
} }
const out: string[] = []; const out: string[] = [];
for (const [wallId, polys] of byWall) { for (const [id, polys] of byId) {
const hit = crossing const hit = crossing
? polys.some((poly) => polyTouchesRect(poly, rect)) ? polys.some((poly) => polyTouchesRect(poly, rect))
: polys.every((poly) => polyEnclosedByRect(poly, rect)); : polys.every((poly) => polyEnclosedByRect(poly, rect));
if (hit) out.push(wallId); if (hit) out.push(id);
} }
return out; return out;
} }
/** Vom Auswahl-Rechteck getroffene Decken-IDs (gleiche Konvention wie {@link marqueeHit}). */
export function marqueeHitCeilings(prims: Primitive[], rect: ModelRect, crossing: boolean): string[] {
return marqueeHitByField(prims, "ceilingId", rect, crossing);
}
/** Vom Auswahl-Rechteck getroffene Raum-IDs (gleiche Konvention wie {@link marqueeHit}). */
export function marqueeHitRooms(prims: Primitive[], rect: ModelRect, crossing: boolean): string[] {
return marqueeHitByField(prims, "roomId", rect, crossing);
}
/** Vom Auswahl-Rechteck getroffene Treppen-IDs (gleiche Konvention wie {@link marqueeHit}). */
export function marqueeHitStairs(prims: Primitive[], rect: ModelRect, crossing: boolean): string[] {
return marqueeHitByField(prims, "stairId", rect, crossing);
}
/** /**
* Vom Auswahl-Rechteck getroffene 2D-Zeichenelement-IDs (gleiche window/crossing- * Vom Auswahl-Rechteck getroffene 2D-Zeichenelement-IDs (gleiche window/crossing-
* Konvention wie {@link marqueeHit}). Sammelt je Drawing2D ALLE seine Punkte aus * Konvention wie {@link marqueeHit}). Sammelt je Drawing2D ALLE seine Punkte aus
+68
View File
@@ -0,0 +1,68 @@
/**
* `marqueeHitCeilings`/`marqueeHitRooms`/`marqueeHitStairs` — Nutzer-Report:
* "Auswahlrahmen kann nicht alle Elemente anwählen, z.B. Decken, Räume und
* Treppen nicht". Vorher las das Auswahl-Rechteck nur `wallId`/`drawingId`
* aus den Polygon-Primitiven; die anderen ID-Felder (die dieselben
* Primitive längst tragen) wurden ignoriert.
*/
import { describe, it, expect } from "vitest";
import { marqueeHit, marqueeHitCeilings, marqueeHitRooms, marqueeHitStairs } from "./geometry";
import type { Primitive } from "../generatePlan";
const NO_HATCH = { pattern: "none" as const, color: "#000" };
function poly(id: Partial<Record<"wallId" | "ceilingId" | "roomId" | "stairId", string>>, pts: { x: number; y: number }[]): Primitive {
return {
kind: "polygon",
pts,
fill: "none",
stroke: "#000",
strokeWidthMm: 0.18,
hatch: NO_HATCH,
...id,
} as Primitive;
}
const square = (x0: number, y0: number, x1: number, y1: number) => [
{ x: x0, y: y0 }, { x: x1, y: y0 }, { x: x1, y: y1 }, { x: x0, y: y1 },
];
describe("Marquee — Decken/Räume/Treppen", () => {
it("marqueeHitCeilings findet eine vollständig eingeschlossene Decke (window)", () => {
const prims = [poly({ ceilingId: "c1" }, square(1, 1, 3, 3))];
const rect = { minX: 0, minY: 0, maxX: 5, maxY: 5 };
expect(marqueeHitCeilings(prims, rect, false)).toEqual(["c1"]);
});
it("marqueeHitRooms ignoriert einen Raum, der nur teilweise im Fenster liegt (window, kein crossing)", () => {
const prims = [poly({ roomId: "r1" }, square(1, 1, 10, 10))];
const rect = { minX: 0, minY: 0, maxX: 5, maxY: 5 };
expect(marqueeHitRooms(prims, rect, false)).toEqual([]);
// Dieselbe Geometrie im Crossing-Modus (rechts→links) trifft trotzdem.
expect(marqueeHitRooms(prims, rect, true)).toEqual(["r1"]);
});
it("marqueeHitStairs findet eine Treppe über mehrere Tritt-Polygone (alle müssen im Fenster liegen)", () => {
const prims = [
poly({ stairId: "s1" }, square(1, 1, 2, 2)),
poly({ stairId: "s1" }, square(2, 1, 3, 2)),
];
const rect = { minX: 0, minY: 0, maxX: 5, maxY: 5 };
expect(marqueeHitStairs(prims, rect, false)).toEqual(["s1"]);
});
it("Wände (marqueeHit) und Decken/Räume/Treppen sind unabhängig voneinander auswertbar", () => {
const prims = [
poly({ wallId: "w1" }, square(0, 0, 1, 1)),
poly({ ceilingId: "c1" }, square(1, 1, 2, 2)),
poly({ roomId: "r1" }, square(2, 2, 3, 3)),
poly({ stairId: "s1" }, square(3, 3, 4, 4)),
];
const rect = { minX: 0, minY: 0, maxX: 5, maxY: 5 };
expect(marqueeHit(prims, rect, false)).toEqual(["w1"]);
expect(marqueeHitCeilings(prims, rect, false)).toEqual(["c1"]);
expect(marqueeHitRooms(prims, rect, false)).toEqual(["r1"]);
expect(marqueeHitStairs(prims, rect, false)).toEqual(["s1"]);
});
});
+7 -5
View File
@@ -331,12 +331,14 @@ export function useMouseSelectionHandlers(opts: {
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/Dächer sind (noch) nicht Teil setSelectedCeilingIds(sel.ceilingIds ?? []);
// des Marquee → beim Aufziehen abwählen. setSelectedStairIds(sel.stairIds ?? []);
setSelectedCeilingIds([]); setSelectedRoomIds(sel.roomIds ?? []);
// Öffnungen/Extrusionen/Stützen/Dächer/Kontext-Objekte sind (noch) nicht
// Teil des Marquee (Nutzer-Report betraf nur Decken/Räume/Treppen; Stütze/
// Dach liessen sich mit derselben `marqueeHitByField`-Mechanik ergänzen,
// falls gewünscht) → beim Aufziehen abwählen.
setSelectedOpeningIds([]); setSelectedOpeningIds([]);
setSelectedStairIds([]);
setSelectedRoomIds([]);
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);