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:
@@ -37,6 +37,9 @@ import {
|
||||
pointSegDistPx,
|
||||
marqueeHit,
|
||||
marqueeHitDrawings,
|
||||
marqueeHitCeilings,
|
||||
marqueeHitRooms,
|
||||
marqueeHitStairs,
|
||||
} from "./planView/geometry";
|
||||
import {
|
||||
polyUsesPattern,
|
||||
@@ -208,6 +211,10 @@ export interface MarqueeSelection {
|
||||
wallIds: string[];
|
||||
/** Getroffene 2D-Zeichenelemente (gleiche window/crossing-Konvention). */
|
||||
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
|
||||
* eingeschlossene Wände; `true` = rechts→links (negativ Δx) → auch berührte/
|
||||
@@ -1825,6 +1832,9 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
onMarquee({
|
||||
wallIds: marqueeHit(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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,24 +222,56 @@ export function marqueeHit(
|
||||
rect: ModelRect,
|
||||
crossing: boolean,
|
||||
): string[] {
|
||||
// Polygone je Wand sammeln (nur Primitive mit Wandbezug zählen für die Auswahl).
|
||||
const byWall = new Map<string, Vec2[][]>();
|
||||
return marqueeHitByField(prims, "wallId", rect, crossing);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (p.kind !== "polygon" || p.wallId == null) continue;
|
||||
const list = byWall.get(p.wallId);
|
||||
if (p.kind !== "polygon") continue;
|
||||
const id = p[field];
|
||||
if (id == null) continue;
|
||||
const list = byId.get(id);
|
||||
if (list) list.push(p.pts);
|
||||
else byWall.set(p.wallId, [p.pts]);
|
||||
else byId.set(id, [p.pts]);
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const [wallId, polys] of byWall) {
|
||||
for (const [id, polys] of byId) {
|
||||
const hit = crossing
|
||||
? polys.some((poly) => polyTouchesRect(poly, rect))
|
||||
: polys.every((poly) => polyEnclosedByRect(poly, rect));
|
||||
if (hit) out.push(wallId);
|
||||
if (hit) out.push(id);
|
||||
}
|
||||
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-
|
||||
* Konvention wie {@link marqueeHit}). Sammelt je Drawing2D ALLE seine Punkte aus
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -331,12 +331,14 @@ export function useMouseSelectionHandlers(opts: {
|
||||
const onPlanMarquee = (sel: MarqueeSelection) => {
|
||||
setSelectedWallIds(sel.wallIds);
|
||||
setSelectedDrawingIds(sel.drawingIds ?? []);
|
||||
// Decken/Öffnungen/Treppen/Räume/Extrusionen/Dächer sind (noch) nicht Teil
|
||||
// des Marquee → beim Aufziehen abwählen.
|
||||
setSelectedCeilingIds([]);
|
||||
setSelectedCeilingIds(sel.ceilingIds ?? []);
|
||||
setSelectedStairIds(sel.stairIds ?? []);
|
||||
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([]);
|
||||
setSelectedStairIds([]);
|
||||
setSelectedRoomIds([]);
|
||||
setSelectedExtrudedSolidIds([]);
|
||||
setSelectedColumnIds([]);
|
||||
setSelectedRoofIds([]);
|
||||
|
||||
Reference in New Issue
Block a user