2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
This commit is contained in:
+231
-1
@@ -24,6 +24,7 @@ import type {
|
||||
} from "../model/types";
|
||||
import { commitTransform } from "../tools/transform";
|
||||
import type { CopyMode, TransformOp, TransformSelection } from "../tools/transform";
|
||||
import { centroid as roomCentroid } from "../geometry/roomArea";
|
||||
import { t } from "../i18n";
|
||||
import type { StoreApi } from "./store";
|
||||
|
||||
@@ -161,6 +162,74 @@ export interface ProjectSlice {
|
||||
* Dicke nur auf diese Wand und teilt keine Presets versehentlich.
|
||||
*/
|
||||
setWallThickness: (wallId: string, thickness: number) => void;
|
||||
|
||||
// ── Decken-Editieren ──────────────────────────────────────────────────────
|
||||
/** Verschiebt EINEN Umriss-Vertex (per Index) einer Decke. */
|
||||
moveCeilingGrip: (ceilingId: string, index: number, pt: Vec2) => void;
|
||||
/** Verschiebt eine Decke (ganzer Umriss) um `delta`. */
|
||||
moveCeilingBy: (ceilingId: string, delta: Vec2) => void;
|
||||
/** Verschiebt eine Umriss-KANTE (beide Vertices) einer Decke um `delta`. */
|
||||
moveCeilingEdge: (
|
||||
ceilingId: string,
|
||||
aIndex: number,
|
||||
bIndex: number,
|
||||
delta: Vec2,
|
||||
) => void;
|
||||
/** Generischer immutabler Patch auf eine Decke (per ID). */
|
||||
updateCeiling: (
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Ceiling>,
|
||||
) => void;
|
||||
/** Setzt die Gesamtdicke einer Decke (thickness-Übersteuerung, Meter). */
|
||||
setCeilingThickness: (ceilingId: string, thickness: number) => void;
|
||||
|
||||
// ── Öffnungs-Editieren (Object-Info-Panel) ────────────────────────────────
|
||||
/** Generischer immutabler Patch auf eine Öffnung (per ID). */
|
||||
updateOpening: (
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Opening>,
|
||||
) => void;
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────────
|
||||
/** Verschiebt eine Treppe (Antritt) um `delta`. */
|
||||
moveStairBy: (stairId: string, delta: Vec2) => void;
|
||||
/** Generischer immutabler Patch auf eine Treppe (per ID). */
|
||||
updateStair: (
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Stair>,
|
||||
) => void;
|
||||
|
||||
// ── Raum-Editieren ──────────────────────────────────────────────────────────
|
||||
/** Verschiebt einen Raum (ganzer Umriss + optionaler Stempel-Anker) um `delta`. */
|
||||
moveRoomBy: (roomId: string, delta: Vec2) => void;
|
||||
/** Verschiebt EINEN Umriss-Vertex (per Index) eines Raums. */
|
||||
moveRoomGrip: (roomId: string, index: number, pt: Vec2) => void;
|
||||
/** Verschiebt eine Umriss-KANTE (beide Vertices) eines Raums um `delta`. */
|
||||
moveRoomEdge: (
|
||||
roomId: string,
|
||||
aIndex: number,
|
||||
bIndex: number,
|
||||
delta: Vec2,
|
||||
) => void;
|
||||
/** Generischer immutabler Patch auf einen Raum (per ID). */
|
||||
updateRoom: (
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Room>,
|
||||
) => void;
|
||||
/** Verschiebt NUR den Stempel-Anker eines Raums um `delta` (Kontur bleibt). */
|
||||
moveRoomStamp: (roomId: string, delta: Vec2) => void;
|
||||
/** Setzt den Stempel-Anker eines Raums absolut. */
|
||||
setRoomStampAnchor: (roomId: string, anchor: Vec2) => void;
|
||||
/** Setzt den Rich-Text des Raum-Stempels. */
|
||||
setRoomStampDoc: (
|
||||
roomId: string,
|
||||
doc: import("../model/types").RichTextDoc,
|
||||
) => void;
|
||||
/** Setzt den strukturierten Raum-Stempel (Feldmodell). */
|
||||
setRoomStamp: (
|
||||
roomId: string,
|
||||
stamp: import("../model/types").RoomStamp,
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,7 +378,13 @@ export function createProjectSlice(
|
||||
p.drawingLevels.filter((z) => z.id !== id),
|
||||
),
|
||||
walls: p.walls.filter((w) => w.floorId !== id),
|
||||
ceilings: (p.ceilings ?? []).filter((c) => c.floorId !== id),
|
||||
stairs: (p.stairs ?? []).filter((s) => s.floorId !== id),
|
||||
rooms: (p.rooms ?? []).filter((r) => r.floorId !== id),
|
||||
doors: p.doors.filter((d) => !removedWallIds.has(d.hostWallId)),
|
||||
openings: (p.openings ?? []).filter(
|
||||
(o) => !removedWallIds.has(o.hostWallId),
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -376,7 +451,10 @@ export function createProjectSlice(
|
||||
const codes = new Set(flattenCategories([src]).map((c) => c.code));
|
||||
const usedByWall = p.walls.some((w) => codes.has(w.categoryCode));
|
||||
const usedByDoor = p.doors.some((d) => codes.has(d.categoryCode));
|
||||
if (usedByWall || usedByDoor) {
|
||||
const usedByOpening = (p.openings ?? []).some((o) =>
|
||||
codes.has(o.categoryCode),
|
||||
);
|
||||
if (usedByWall || usedByDoor || usedByOpening) {
|
||||
window.alert(t("alert.layerInUse", { code: src.code, name: src.name }));
|
||||
return p;
|
||||
}
|
||||
@@ -630,6 +708,158 @@ export function createProjectSlice(
|
||||
|
||||
setWallThickness: (wallId, thickness) =>
|
||||
setProject((p) => setWallThickness(p, wallId, thickness)),
|
||||
|
||||
// ── Decken-Editieren ──────────────────────────────────────────────────
|
||||
moveCeilingGrip: (ceilingId, index, pt) =>
|
||||
setProject((p) => mapCeiling(p, ceilingId, (c) => ({
|
||||
...c,
|
||||
outline: c.outline.map((v, i) => (i === index ? pt : v)),
|
||||
}))),
|
||||
|
||||
moveCeilingBy: (ceilingId, delta) =>
|
||||
setProject((p) => mapCeiling(p, ceilingId, (c) => ({
|
||||
...c,
|
||||
outline: c.outline.map((v) => ({ x: v.x + delta.x, y: v.y + delta.y })),
|
||||
}))),
|
||||
|
||||
moveCeilingEdge: (ceilingId, aIndex, bIndex, delta) =>
|
||||
setProject((p) => mapCeiling(p, ceilingId, (c) => ({
|
||||
...c,
|
||||
outline: c.outline.map((v, i) =>
|
||||
i === aIndex || i === bIndex ? { x: v.x + delta.x, y: v.y + delta.y } : v,
|
||||
),
|
||||
}))),
|
||||
|
||||
updateCeiling: (id, patch) =>
|
||||
setProject((p) => mapCeiling(p, id, (c) => ({ ...c, ...patch }))),
|
||||
|
||||
setCeilingThickness: (ceilingId, thickness) =>
|
||||
setProject((p) =>
|
||||
!isFinite(thickness) || thickness <= 0
|
||||
? p
|
||||
: mapCeiling(p, ceilingId, (c) => ({ ...c, thickness })),
|
||||
),
|
||||
|
||||
// ── Öffnungs-Editieren ────────────────────────────────────────────────
|
||||
updateOpening: (id, patch) =>
|
||||
setProject((p) => ({
|
||||
...p,
|
||||
openings: (p.openings ?? []).map((o) => (o.id === id ? { ...o, ...patch } : o)),
|
||||
})),
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────
|
||||
moveStairBy: (stairId, delta) =>
|
||||
setProject((p) =>
|
||||
mapStair(p, stairId, (s) => ({
|
||||
...s,
|
||||
start: { x: s.start.x + delta.x, y: s.start.y + delta.y },
|
||||
...(s.center
|
||||
? { center: { x: s.center.x + delta.x, y: s.center.y + delta.y } }
|
||||
: {}),
|
||||
})),
|
||||
),
|
||||
|
||||
updateStair: (id, patch) =>
|
||||
setProject((p) => mapStair(p, id, (s) => ({ ...s, ...patch }))),
|
||||
|
||||
// ── Raum-Editieren ──────────────────────────────────────────────────────
|
||||
moveRoomBy: (roomId, delta) =>
|
||||
setProject((p) =>
|
||||
mapRoom(p, roomId, (r) => ({
|
||||
...r,
|
||||
boundary: r.boundary.map((v) => ({ x: v.x + delta.x, y: v.y + delta.y })),
|
||||
...(r.stampAnchor
|
||||
? {
|
||||
stampAnchor: {
|
||||
x: r.stampAnchor.x + delta.x,
|
||||
y: r.stampAnchor.y + delta.y,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
),
|
||||
|
||||
moveRoomGrip: (roomId, index, pt) =>
|
||||
setProject((p) =>
|
||||
mapRoom(p, roomId, (r) => ({
|
||||
...r,
|
||||
boundary: r.boundary.map((v, i) => (i === index ? pt : v)),
|
||||
})),
|
||||
),
|
||||
|
||||
moveRoomEdge: (roomId, aIndex, bIndex, delta) =>
|
||||
setProject((p) =>
|
||||
mapRoom(p, roomId, (r) => ({
|
||||
...r,
|
||||
boundary: r.boundary.map((v, i) =>
|
||||
i === aIndex || i === bIndex
|
||||
? { x: v.x + delta.x, y: v.y + delta.y }
|
||||
: v,
|
||||
),
|
||||
})),
|
||||
),
|
||||
|
||||
updateRoom: (id, patch) =>
|
||||
setProject((p) => mapRoom(p, id, (r) => ({ ...r, ...patch }))),
|
||||
|
||||
moveRoomStamp: (roomId, delta) =>
|
||||
setProject((p) =>
|
||||
mapRoom(p, roomId, (r) => {
|
||||
const base = r.stampAnchor ?? roomCentroid(r.boundary);
|
||||
return {
|
||||
...r,
|
||||
stampAnchor: { x: base.x + delta.x, y: base.y + delta.y },
|
||||
};
|
||||
}),
|
||||
),
|
||||
|
||||
setRoomStampAnchor: (roomId, anchor) =>
|
||||
setProject((p) => mapRoom(p, roomId, (r) => ({ ...r, stampAnchor: anchor }))),
|
||||
|
||||
setRoomStampDoc: (roomId, doc) =>
|
||||
setProject((p) => mapRoom(p, roomId, (r) => ({ ...r, stampDoc: doc }))),
|
||||
|
||||
setRoomStamp: (roomId, stamp) =>
|
||||
setProject((p) => mapRoom(p, roomId, (r) => ({ ...r, stamp }))),
|
||||
};
|
||||
}
|
||||
|
||||
/** Immutabler Map über einen Raum (per ID); No-op ohne Treffer. */
|
||||
function mapRoom(
|
||||
project: Project,
|
||||
roomId: string,
|
||||
fn: (r: import("../model/types").Room) => import("../model/types").Room,
|
||||
): Project {
|
||||
const rooms = project.rooms ?? [];
|
||||
return {
|
||||
...project,
|
||||
rooms: rooms.map((r) => (r.id === roomId ? fn(r) : r)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Immutabler Map über eine Treppe (per ID); No-op ohne Treffer. */
|
||||
function mapStair(
|
||||
project: Project,
|
||||
stairId: string,
|
||||
fn: (s: import("../model/types").Stair) => import("../model/types").Stair,
|
||||
): Project {
|
||||
const stairs = project.stairs ?? [];
|
||||
return {
|
||||
...project,
|
||||
stairs: stairs.map((s) => (s.id === stairId ? fn(s) : s)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Immutabler Map über eine Decke (per ID); No-op ohne Treffer. */
|
||||
function mapCeiling(
|
||||
project: Project,
|
||||
ceilingId: string,
|
||||
fn: (c: import("../model/types").Ceiling) => import("../model/types").Ceiling,
|
||||
): Project {
|
||||
const ceilings = project.ceilings ?? [];
|
||||
return {
|
||||
...project,
|
||||
ceilings: ceilings.map((c) => (c.id === ceilingId ? fn(c) : c)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+325
-3
@@ -10,17 +10,38 @@
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { flattenCategories, getWallType, wallTypeThickness } from "../model/types";
|
||||
import {
|
||||
ceilingThickness,
|
||||
flattenCategories,
|
||||
getCeilingType,
|
||||
getWallType,
|
||||
wallTypeThickness,
|
||||
} from "../model/types";
|
||||
import type {
|
||||
Ceiling,
|
||||
Drawing2D,
|
||||
Drawing2DGeom,
|
||||
LayerCategory,
|
||||
Opening,
|
||||
Project,
|
||||
Room,
|
||||
SiaCategory,
|
||||
Stair,
|
||||
StairShape,
|
||||
VerticalAnchor,
|
||||
Wall,
|
||||
WallReferenceLine,
|
||||
} from "../model/types";
|
||||
import { nextFloorAbove, wallVerticalExtent } from "../model/wall";
|
||||
import { evaluateRoom } from "../geometry/roomArea";
|
||||
import {
|
||||
ceilingVerticalExtent,
|
||||
nextFloorAbove,
|
||||
stairVerticalExtent,
|
||||
wallVerticalExtent,
|
||||
} from "../model/wall";
|
||||
import { ceilingArea, outlineBBox } from "../geometry/ceiling";
|
||||
import { openingGapQuad, openingVerticalExtent } from "../geometry/opening";
|
||||
import { stairGeometry, stairBBox } from "../geometry/stair";
|
||||
|
||||
/** Eintrag für die WallType-Auswahl (Preset-Dropdown) im Object-Info-Panel. */
|
||||
export interface WallTypeChoice {
|
||||
@@ -68,6 +89,102 @@ export interface WallInfo {
|
||||
zTop: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decken-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau
|
||||
* einer Decke gesetzt). Anzeigefertige Werte + Listen für die Dropdowns.
|
||||
*/
|
||||
export interface CeilingInfo {
|
||||
wallTypeId: string;
|
||||
/** Aktuelle Gesamtdicke (Meter). */
|
||||
thickness: number;
|
||||
/** Ob der aktuelle Aufbau-Typ einschichtig ist (1 Schicht). */
|
||||
singleLayer: boolean;
|
||||
/** Grundfläche der Decke (m²). */
|
||||
area: number;
|
||||
/** Verfügbare Aufbau-Typ-Presets (wie WallType). */
|
||||
wallTypes: WallTypeChoice[];
|
||||
/** Geschoss, dem die Decke zugeordnet ist. */
|
||||
floorId: string;
|
||||
floorName: string;
|
||||
/** Alle Geschosse (für die OK-„an Geschoss gebunden"-Auswahl). */
|
||||
floors: FloorChoice[];
|
||||
/** OK-Bindung (undefined = Geschoss-Oberkante). */
|
||||
top?: VerticalAnchor;
|
||||
/** Aufgelöste absolute UK/OK (Meter). */
|
||||
zBottom: number;
|
||||
zTop: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnungs-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl
|
||||
* genau einer Öffnung gesetzt). Anzeigefertige Werte + Wirts-Wand-Bezug.
|
||||
*/
|
||||
export interface OpeningInfo {
|
||||
kind: "window" | "door";
|
||||
/** Wirts-Wand (ID + Name/Geschoss zur Anzeige). */
|
||||
hostWallId: string;
|
||||
hostWallName: string;
|
||||
/** Abstand vom Wand-Startpunkt (Meter). */
|
||||
position: number;
|
||||
width: number;
|
||||
height: number;
|
||||
/** Brüstungshöhe (Meter); 0 bei Türen. */
|
||||
sillHeight: number;
|
||||
/** Nur Tür: Anschlag/Aufschlag/Winkel/Richtung. */
|
||||
hinge?: "start" | "end";
|
||||
swing?: "left" | "right";
|
||||
swingAngle?: number;
|
||||
openingDir?: "in" | "out";
|
||||
/** Aufgelöste absolute UK/OK (Meter). */
|
||||
zBottom: number;
|
||||
zTop: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Treppen-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau
|
||||
* einer Treppe gesetzt). Anzeigefertige Werte + Listen für die Dropdowns.
|
||||
*/
|
||||
export interface StairInfo {
|
||||
shape: StairShape;
|
||||
/** Laufbreite (Meter). */
|
||||
width: number;
|
||||
/** Stufenanzahl (Setzstufen). */
|
||||
stepCount: number;
|
||||
/** Gesamt-Steighöhe (Meter, OKFF → OKFF). */
|
||||
totalRise: number;
|
||||
/** Abgeleitete Steigungshöhe (Meter) = totalRise / stepCount. */
|
||||
riserHeight: number;
|
||||
/** Abgeleitete Auftrittstiefe (Meter). */
|
||||
treadDepth: number;
|
||||
/** Laufrichtung aufwärts (Auf-/Abpfeil-Richtung). */
|
||||
up: boolean;
|
||||
/** Geschoss, dem die Treppe zugeordnet ist. */
|
||||
floorId: string;
|
||||
floorName: string;
|
||||
/** Aufgelöste absolute UK/OK (Meter). */
|
||||
zBottom: number;
|
||||
zTop: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raum-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau
|
||||
* eines Raums gesetzt). Anzeigefertige Werte; Fläche/Umfang werden aus dem
|
||||
* Umriss abgeleitet (nie gespeichert).
|
||||
*/
|
||||
export interface RoomInfo {
|
||||
/** Raum-Name (editierbar). */
|
||||
name: string;
|
||||
/** SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF). */
|
||||
siaCategory: SiaCategory;
|
||||
/** Netto-anrechenbare Fläche (m²). */
|
||||
area: number;
|
||||
/** Umfang des Umrisses (m). */
|
||||
perimeter: number;
|
||||
/** Geschoss, dem der Raum zugeordnet ist. */
|
||||
floorId: string;
|
||||
floorName: string;
|
||||
}
|
||||
|
||||
/** Achsparallele Bounding-Box eines Elements in Modell-Metern. */
|
||||
export interface SelectionBBox {
|
||||
minX: number;
|
||||
@@ -82,7 +199,7 @@ export interface SelectionBBox {
|
||||
* gezeichneten Wert zeigt. `fillHatchId`/`closed` sind nur für Drawing2D sinnvoll.
|
||||
*/
|
||||
export interface Selection {
|
||||
kind: "wall" | "drawing2d" | "door";
|
||||
kind: "wall" | "ceiling" | "opening" | "stair" | "room" | "drawing2d" | "door";
|
||||
id: string;
|
||||
/** Grafik-Kategorie (Ebene) des Elements. */
|
||||
categoryCode: string;
|
||||
@@ -109,6 +226,14 @@ export interface Selection {
|
||||
* (Wandtyp/Dicke), Referenzgeschoss + UK/OK für den Wand-Abschnitt im Panel.
|
||||
*/
|
||||
wall?: WallInfo;
|
||||
/** Decken-Attribute (nur bei `kind === "ceiling"`). */
|
||||
ceiling?: CeilingInfo;
|
||||
/** Öffnungs-Attribute (nur bei `kind === "opening"`). */
|
||||
opening?: OpeningInfo;
|
||||
/** Treppen-Attribute (nur bei `kind === "stair"`). */
|
||||
stair?: StairInfo;
|
||||
/** Raum-Attribute (nur bei `kind === "room"`). */
|
||||
room?: RoomInfo;
|
||||
}
|
||||
|
||||
const WALL_FALLBACK_MM = 0.18;
|
||||
@@ -222,6 +347,183 @@ function wallSelection(project: Project, wall: Wall): Selection {
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für eine Decke ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||
function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
|
||||
const cat = findCategory(project.layers, ceiling.categoryCode);
|
||||
const color = ceiling.color ?? cat?.color ?? WALL_FALLBACK_COLOR;
|
||||
const weightMm = cat?.lw ?? WALL_FALLBACK_MM;
|
||||
const box = outlineBBox(ceiling.outline);
|
||||
const wt = getCeilingType(project, ceiling);
|
||||
const thickness = ceilingThickness(project, ceiling);
|
||||
const floor = project.drawingLevels.find((z) => z.id === ceiling.floorId);
|
||||
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
|
||||
const ceilingInfo: CeilingInfo = {
|
||||
wallTypeId: ceiling.wallTypeId,
|
||||
thickness,
|
||||
singleLayer: wt.layers.length <= 1,
|
||||
area: ceilingArea(ceiling.outline),
|
||||
wallTypes: project.wallTypes.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
thickness: wallTypeThickness(t),
|
||||
layerCount: t.layers.length,
|
||||
})),
|
||||
floorId: ceiling.floorId,
|
||||
floorName: floor?.name ?? ceiling.floorId,
|
||||
floors: project.drawingLevels
|
||||
.filter((z) => z.kind === "floor")
|
||||
.map((z) => ({ id: z.id, name: z.name })),
|
||||
top: ceiling.top,
|
||||
zBottom,
|
||||
zTop,
|
||||
};
|
||||
return {
|
||||
kind: "ceiling",
|
||||
id: ceiling.id,
|
||||
categoryCode: ceiling.categoryCode,
|
||||
color,
|
||||
weightMm,
|
||||
fillHatchId: undefined,
|
||||
fillColor: undefined,
|
||||
closed: true,
|
||||
bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY },
|
||||
ceiling: ceilingInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für eine Öffnung ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||
function openingSelection(project: Project, o: Opening): Selection {
|
||||
const cat = findCategory(project.layers, o.categoryCode);
|
||||
const color = o.color ?? cat?.color ?? WALL_FALLBACK_COLOR;
|
||||
const weightMm = cat?.lw ?? WALL_FALLBACK_MM;
|
||||
const wall = project.walls.find((w) => w.id === o.hostWallId);
|
||||
const floor = wall
|
||||
? project.drawingLevels.find((z) => z.id === wall.floorId)
|
||||
: undefined;
|
||||
// Bounding-Box aus dem Lücken-Quad (volle Wanddicke); Fallback: leere Box.
|
||||
const quad = wall ? openingGapQuad(project, wall, o) : null;
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
for (const p of quad ?? []) {
|
||||
minX = Math.min(minX, p.x);
|
||||
minY = Math.min(minY, p.y);
|
||||
maxX = Math.max(maxX, p.x);
|
||||
maxY = Math.max(maxY, p.y);
|
||||
}
|
||||
if (!isFinite(minX)) {
|
||||
minX = minY = maxX = maxY = 0;
|
||||
}
|
||||
const ext = wall
|
||||
? openingVerticalExtent(project, wall, o)
|
||||
: { zBottom: 0, zTop: o.height };
|
||||
const openingInfo: OpeningInfo = {
|
||||
kind: o.kind,
|
||||
hostWallId: o.hostWallId,
|
||||
hostWallName: floor ? `${floor.name}` : o.hostWallId,
|
||||
position: o.position,
|
||||
width: o.width,
|
||||
height: o.height,
|
||||
sillHeight: o.sillHeight,
|
||||
hinge: o.hinge,
|
||||
swing: o.swing,
|
||||
swingAngle: o.swingAngle,
|
||||
openingDir: o.openingDir,
|
||||
zBottom: ext.zBottom,
|
||||
zTop: ext.zTop,
|
||||
};
|
||||
return {
|
||||
kind: "opening",
|
||||
id: o.id,
|
||||
categoryCode: o.categoryCode,
|
||||
color,
|
||||
weightMm,
|
||||
fillHatchId: undefined,
|
||||
fillColor: undefined,
|
||||
closed: undefined,
|
||||
bbox: { minX, minY, maxX, maxY },
|
||||
opening: openingInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für eine Treppe ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */
|
||||
function stairSelection(project: Project, s: Stair): Selection {
|
||||
const cat = findCategory(project.layers, s.categoryCode);
|
||||
const color = s.color ?? cat?.color ?? WALL_FALLBACK_COLOR;
|
||||
const weightMm = cat?.lw ?? WALL_FALLBACK_MM;
|
||||
const { zBottom, zTop } = stairVerticalExtent(project, s);
|
||||
const totalRise = zTop - zBottom;
|
||||
const geo = stairGeometry(s, totalRise);
|
||||
const box = stairBBox(geo);
|
||||
const floor = project.drawingLevels.find((z) => z.id === s.floorId);
|
||||
const stairInfo: StairInfo = {
|
||||
shape: s.shape,
|
||||
width: s.width,
|
||||
stepCount: s.stepCount,
|
||||
totalRise,
|
||||
riserHeight: geo.riserHeight,
|
||||
treadDepth: geo.treadDepth,
|
||||
up: s.up !== false,
|
||||
floorId: s.floorId,
|
||||
floorName: floor?.name ?? s.floorId,
|
||||
zBottom,
|
||||
zTop,
|
||||
};
|
||||
return {
|
||||
kind: "stair",
|
||||
id: s.id,
|
||||
categoryCode: s.categoryCode,
|
||||
color,
|
||||
weightMm,
|
||||
fillHatchId: undefined,
|
||||
fillColor: undefined,
|
||||
closed: undefined,
|
||||
bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY },
|
||||
stair: stairInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für einen Raum ableiten (Fläche/Umfang aus dem Umriss abgeleitet). */
|
||||
function roomSelection(project: Project, r: Room): Selection {
|
||||
const cat = findCategory(project.layers, r.categoryCode);
|
||||
const color = r.color ?? cat?.color ?? WALL_FALLBACK_COLOR;
|
||||
const weightMm = cat?.lw ?? WALL_FALLBACK_MM;
|
||||
const res = evaluateRoom(r.boundary, r.siaCategory);
|
||||
const floor = project.drawingLevels.find((z) => z.id === r.floorId);
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
for (const p of r.boundary) {
|
||||
minX = Math.min(minX, p.x);
|
||||
minY = Math.min(minY, p.y);
|
||||
maxX = Math.max(maxX, p.x);
|
||||
maxY = Math.max(maxY, p.y);
|
||||
}
|
||||
if (!isFinite(minX)) minX = minY = maxX = maxY = 0;
|
||||
const roomInfo: RoomInfo = {
|
||||
name: r.name,
|
||||
siaCategory: r.siaCategory,
|
||||
area: res.netArea,
|
||||
perimeter: res.perimeter,
|
||||
floorId: r.floorId,
|
||||
floorName: floor?.name ?? r.floorId,
|
||||
};
|
||||
return {
|
||||
kind: "room",
|
||||
id: r.id,
|
||||
categoryCode: r.categoryCode,
|
||||
color,
|
||||
weightMm,
|
||||
fillHatchId: undefined,
|
||||
fillColor: undefined,
|
||||
closed: true,
|
||||
bbox: { minX, minY, maxX, maxY },
|
||||
room: roomInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** Selektion für ein 2D-Zeichenelement ableiten (gleiche Auflösung wie generatePlan). */
|
||||
function drawingSelection(project: Project, d: Drawing2D): Selection {
|
||||
const ls = d.lineStyleId
|
||||
@@ -254,15 +556,35 @@ export function deriveSelection(
|
||||
project: Project,
|
||||
selectedWallIds: string[],
|
||||
selectedDrawingId: string | null,
|
||||
selectedCeilingId: string | null = null,
|
||||
selectedOpeningId: string | null = null,
|
||||
selectedStairId: string | null = null,
|
||||
selectedRoomId: string | null = null,
|
||||
): Selection | null {
|
||||
if (selectedDrawingId) {
|
||||
const d = project.drawings2d.find((x) => x.id === selectedDrawingId);
|
||||
return d ? drawingSelection(project, d) : null;
|
||||
}
|
||||
if (selectedOpeningId) {
|
||||
const o = (project.openings ?? []).find((x) => x.id === selectedOpeningId);
|
||||
if (o) return openingSelection(project, o);
|
||||
}
|
||||
const wallId = selectedWallIds[0];
|
||||
if (wallId) {
|
||||
const w = project.walls.find((x) => x.id === wallId);
|
||||
return w ? wallSelection(project, w) : null;
|
||||
}
|
||||
if (selectedCeilingId) {
|
||||
const c = (project.ceilings ?? []).find((x) => x.id === selectedCeilingId);
|
||||
return c ? ceilingSelection(project, c) : null;
|
||||
}
|
||||
if (selectedStairId) {
|
||||
const s = (project.stairs ?? []).find((x) => x.id === selectedStairId);
|
||||
return s ? stairSelection(project, s) : null;
|
||||
}
|
||||
if (selectedRoomId) {
|
||||
const r = (project.rooms ?? []).find((x) => x.id === selectedRoomId);
|
||||
return r ? roomSelection(project, r) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,22 @@ export interface SelectionSlice {
|
||||
// Gewählte 2D-Zeichenelemente als MENGE (Marquee/Mehrfach/Einzel) — getrennt
|
||||
// von der Wand-Auswahl. Einzel-Auswahl = Array der Länge 1.
|
||||
selectedDrawingIds: string[];
|
||||
// Gewählte Decken als MENGE von IDs — getrennt von Wand-/2D-Auswahl.
|
||||
selectedCeilingIds: string[];
|
||||
// Gewählte Öffnungen (Fenster/Türen) als MENGE von IDs — getrennt.
|
||||
selectedOpeningIds: string[];
|
||||
// Gewählte Treppen als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||
selectedStairIds: string[];
|
||||
// Gewählte Räume als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||
selectedRoomIds: string[];
|
||||
|
||||
setSelectedWallIds: (ids: string[]) => void;
|
||||
setSelectedDrawingIds: (ids: string[]) => void;
|
||||
/** Auswahl (Wände + 2D-Elemente) vollständig leeren. */
|
||||
setSelectedCeilingIds: (ids: string[]) => void;
|
||||
setSelectedOpeningIds: (ids: string[]) => void;
|
||||
setSelectedStairIds: (ids: string[]) => void;
|
||||
setSelectedRoomIds: (ids: string[]) => void;
|
||||
/** Auswahl (Wände + Decken + Öffnungen + Treppen + 2D-Elemente) leeren. */
|
||||
clearSelection: () => void;
|
||||
}
|
||||
|
||||
@@ -25,9 +37,24 @@ export function createSelectionSlice(
|
||||
return {
|
||||
selectedWallIds: [],
|
||||
selectedDrawingIds: [],
|
||||
selectedCeilingIds: [],
|
||||
selectedOpeningIds: [],
|
||||
selectedStairIds: [],
|
||||
selectedRoomIds: [],
|
||||
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
||||
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
||||
setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }),
|
||||
setSelectedOpeningIds: (ids) => set({ selectedOpeningIds: ids }),
|
||||
setSelectedStairIds: (ids) => set({ selectedStairIds: ids }),
|
||||
setSelectedRoomIds: (ids) => set({ selectedRoomIds: ids }),
|
||||
clearSelection: () =>
|
||||
set({ selectedWallIds: [], selectedDrawingIds: [] }),
|
||||
set({
|
||||
selectedWallIds: [],
|
||||
selectedDrawingIds: [],
|
||||
selectedCeilingIds: [],
|
||||
selectedOpeningIds: [],
|
||||
selectedStairIds: [],
|
||||
selectedRoomIds: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user