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,
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
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([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user