Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)

Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md -Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
This commit is contained in:
2026-07-09 00:57:29 +02:00
parent 889cbb2c12
commit 35299307d6
131 changed files with 26501 additions and 852 deletions
+180 -2
View File
@@ -21,20 +21,26 @@ import {
import type {
AttributeSource,
Ceiling,
Column,
ColumnProfile,
Drawing2D,
Drawing2DGeom,
ExtrudedSolid,
LayerCategory,
Opening,
Project,
Room,
SiaCategory,
SliceTermination,
Stair,
StairShape,
VerticalAnchor,
Wall,
WallReferenceLine,
} from "../model/types";
import { evaluateRoom } from "../geometry/roomArea";
import { columnVerticalExtent } from "../model/wall";
import { columnFootprint } from "../geometry/column";
import { evaluateRoom, polygonArea } from "../geometry/roomArea";
import {
ceilingVerticalExtent,
nextFloorAbove,
@@ -70,6 +76,8 @@ export interface FloorChoice {
*/
export interface WallInfo {
referenceLine: WallReferenceLine;
/** Terminierungs-Regel am Deckenanschluss (Default "both" = heutiges Verhalten). */
sliceTermination: SliceTermination;
wallTypeId: string;
/** Aktuelle Gesamtdicke (Meter). */
thickness: number;
@@ -91,6 +99,14 @@ export interface WallInfo {
/** Aufgelöste absolute UK/OK (Meter) + abgeleitete Höhe. */
zBottom: number;
zTop: number;
/** Achslänge der Wand (Meter). */
length: number;
/** Bruttovolumen = Länge × Dicke × Höhe (m³, ohne Öffnungsabzug). */
grossVolume: number;
/** Summe der Öffnungsvolumina (Fenster/Türen) dieser Wand (m³). */
openingVolume: number;
/** Nettovolumen = Brutto Öffnungen (m³, ≥ 0). */
netVolume: number;
}
/**
@@ -106,6 +122,8 @@ export interface CeilingInfo {
singleLayer: boolean;
/** Grundfläche der Decke (m²). */
area: number;
/** Volumen = Fläche × Dicke (m³). */
volume: number;
/** Verfügbare Deckentyp-Presets (dediziert, `project.ceilingTypes`). */
ceilingTypes: WallTypeChoice[];
/** Geschoss, dem die Decke zugeordnet ist. */
@@ -115,6 +133,8 @@ export interface CeilingInfo {
floors: FloorChoice[];
/** OK-Bindung (undefined = Geschoss-Oberkante). */
top?: VerticalAnchor;
/** UK-Bindung (undefined = OK Dicke). */
bottom?: VerticalAnchor;
/** Aufgelöste absolute UK/OK (Meter). */
zBottom: number;
zTop: number;
@@ -146,6 +166,8 @@ export interface OpeningInfo {
doorType?: "normal" | "wandoeffnung";
/** Nur Tür: Sturzlinien (keine/innen/aussen/beide). Fehlt → "beide". */
lintelLines?: "keine" | "innen" | "aussen" | "beide";
/** Zugewiesener Tür-/Fenstertyp (Bibliothek, `Opening.typeId`) oder undefined. */
typeId?: string;
/** Aufgelöste absolute UK/OK (Meter). */
zBottom: number;
zTop: number;
@@ -171,6 +193,8 @@ export interface StairInfo {
up: boolean;
/** Referenzpunkt der Laufbreite (links/mitte/rechts). Fehlt → "mitte". */
referenz?: "links" | "mitte" | "rechts";
/** Zugewiesener Treppentyp (Bibliothek, `Stair.typeId`) oder undefined. */
typeId?: string;
/** Geschoss, dem die Treppe zugeordnet ist. */
floorId: string;
floorName: string;
@@ -198,6 +222,41 @@ export interface RoomInfo {
floorName: string;
}
/**
* Extrusions-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl
* genau eines extrudierten Körpers, truck-Integration, gesetzt).
*/
export interface ExtrudedSolidInfo {
/** Extrusionshöhe in Metern (editierbar). */
height: number;
/** Verjüngung 0 (Prisma) … 1 (Spitze), editierbar. */
taper: number;
/** Grundfläche des Profils (m²). */
area: number;
/** Geschoss, dessen `baseElevation` die UK des Körpers bestimmt. */
floorId: string;
floorName: string;
}
/**
* Stützen-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau
* einer Stütze gesetzt). Anzeigefertige Werte für die Panel-Felder.
*/
export interface ColumnInfo {
/** Profil (Rechteck/Kreis) — bestimmt, welche Masse editierbar sind. */
profile: ColumnProfile;
/** Höhe (Meter, editierbar). */
height: number;
/** Drehung (Grad, editierbar) — intern in Radiant gespeichert. */
rotationDeg: number;
/** Geschoss, dem die Stütze zugeordnet ist. */
floorId: string;
floorName: string;
/** Aufgelöste absolute UK/OK (Meter). */
zBottom: number;
zTop: number;
}
/** Achsparallele Bounding-Box eines Elements in Modell-Metern. */
export interface SelectionBBox {
minX: number;
@@ -212,7 +271,16 @@ export interface SelectionBBox {
* gezeichneten Wert zeigt. `fillHatchId`/`closed` sind nur für Drawing2D sinnvoll.
*/
export interface Selection {
kind: "wall" | "ceiling" | "opening" | "stair" | "room" | "drawing2d" | "door";
kind:
| "wall"
| "ceiling"
| "opening"
| "stair"
| "room"
| "drawing2d"
| "door"
| "extrudedSolid"
| "column";
id: string;
/** Grafik-Kategorie (Ebene) des Elements. */
categoryCode: string;
@@ -276,6 +344,10 @@ export interface Selection {
stair?: StairInfo;
/** Raum-Attribute (nur bei `kind === "room"`). */
room?: RoomInfo;
/** Extrusions-Attribute (nur bei `kind === "extrudedSolid"`). */
extrudedSolid?: ExtrudedSolidInfo;
/** Stützen-Attribute (nur bei `kind === "column"`). */
column?: ColumnInfo;
}
const WALL_FALLBACK_MM = 0.18;
@@ -354,8 +426,18 @@ function wallSelection(project: Project, wall: Wall): Selection {
const floor = project.drawingLevels.find((z) => z.id === wall.floorId);
const { zBottom, zTop } = wallVerticalExtent(project, wall);
const above = nextFloorAbove(project, wall);
// Volumen aus Achslänge × Gesamtdicke × Höhe, abzüglich der Öffnungen (jede
// Öffnung durchbricht die volle Wanddicke: Breite × Höhe × Dicke).
const length = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
const wallHeight = Math.max(0, zTop - zBottom);
const grossVolume = length * thickness * wallHeight;
const openingVolume = (project.openings ?? [])
.filter((o) => o.hostWallId === wall.id)
.reduce((sum, o) => sum + o.width * o.height * thickness, 0);
const netVolume = Math.max(0, grossVolume - openingVolume);
const wallInfo: WallInfo = {
referenceLine: wall.referenceLine ?? "center",
sliceTermination: wall.sliceTermination ?? "both",
wallTypeId: wall.wallTypeId,
thickness,
singleLayer: wt.layers.length <= 1,
@@ -376,6 +458,10 @@ function wallSelection(project: Project, wall: Wall): Selection {
top: wall.top,
zBottom,
zTop,
length,
grossVolume,
openingVolume,
netVolume,
};
// bbox aus der Wandachse (start/end); die Schichtdicke bleibt unberücksichtigt,
// damit die Box der Achs-Geometrie folgt (genügt für die Paletten).
@@ -415,6 +501,7 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
thickness,
singleLayer: wt.layers.length <= 1,
area: ceilingArea(ceiling.outline),
volume: ceilingArea(ceiling.outline) * thickness,
ceilingTypes: (project.ceilingTypes ?? []).map((t) => ({
id: t.id,
name: t.name,
@@ -428,6 +515,7 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
.filter((z) => z.kind === "floor")
.map((z) => ({ id: z.id, name: z.name })),
top: ceiling.top,
bottom: ceiling.bottom,
zBottom,
zTop,
};
@@ -494,6 +582,7 @@ function openingSelection(project: Project, o: Opening): Selection {
wingCount: o.wingCount,
doorType: o.doorType,
lintelLines: o.lintelLines,
typeId: o.typeId,
zBottom: ext.zBottom,
zTop: ext.zTop,
};
@@ -530,6 +619,7 @@ function stairSelection(project: Project, s: Stair): Selection {
treadDepth: geo.treadDepth,
up: s.up !== false,
referenz: s.referenz,
typeId: s.typeId,
floorId: s.floorId,
floorName: floor?.name ?? s.floorId,
zBottom,
@@ -589,6 +679,84 @@ function roomSelection(project: Project, r: Room): Selection {
};
}
/** Auswahl-Farbe der Extrusions-Footprints (identisch zum Grundriss-Umriss/3D-Orange). */
const EXTRUSION_COLOR = "#d98c40";
/** Selektion für einen extrudierten Körper ableiten (truck-Integration). */
function extrudedSolidSelection(project: Project, s: ExtrudedSolid): Selection {
const floor = project.drawingLevels.find((z) => z.id === s.levelId);
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
for (const p of s.points) {
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 info: ExtrudedSolidInfo = {
height: s.height,
taper: s.taper ?? 0,
area: polygonArea(s.points),
floorId: s.levelId,
floorName: floor?.name ?? s.levelId,
};
return {
kind: "extrudedSolid",
id: s.id,
categoryCode: "",
color: EXTRUSION_COLOR,
weightMm: WALL_FALLBACK_MM,
fillHatchId: null,
fillColor: null,
closed: true,
bbox: { minX, minY, maxX, maxY },
extrudedSolid: info,
};
}
/** Umriss-/Auswahlfarbe der Stützen-Poché (neutrales Tragwerks-Grau). */
const COLUMN_COLOR = "#3a3f47";
/** Selektion für eine Stütze (Column) ableiten. */
function columnSelection(project: Project, col: Column): Selection {
const floor = project.drawingLevels.find((z) => z.id === col.floorId);
const category = findCategory(project.layers, col.categoryCode);
const pts = columnFootprint(col);
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
for (const p of pts) {
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 { zBottom, zTop } = columnVerticalExtent(project, col);
const info: ColumnInfo = {
profile: col.profile,
height: col.height,
rotationDeg: ((col.rotation ?? 0) * 180) / Math.PI,
floorId: col.floorId,
floorName: floor?.name ?? col.floorId,
zBottom,
zTop,
};
return {
kind: "column",
id: col.id,
categoryCode: col.categoryCode,
color: col.color ?? category?.color ?? COLUMN_COLOR,
weightMm: WALL_FALLBACK_MM,
bbox: { minX, minY, maxX, maxY },
column: info,
};
}
/** Selektion für ein 2D-Zeichenelement ableiten (gleiche Auflösung wie generatePlan). */
function drawingSelection(project: Project, d: Drawing2D): Selection {
const ls = d.lineStyleId
@@ -640,11 +808,21 @@ export function deriveSelection(
selectedOpeningId: string | null = null,
selectedStairId: string | null = null,
selectedRoomId: string | null = null,
selectedExtrudedSolidId: string | null = null,
selectedColumnId: string | null = null,
): Selection | null {
if (selectedDrawingId) {
const d = project.drawings2d.find((x) => x.id === selectedDrawingId);
return d ? drawingSelection(project, d) : null;
}
if (selectedExtrudedSolidId) {
const s = (project.extrudedSolids ?? []).find((x) => x.id === selectedExtrudedSolidId);
return s ? extrudedSolidSelection(project, s) : null;
}
if (selectedColumnId) {
const c = (project.columns ?? []).find((x) => x.id === selectedColumnId);
return c ? columnSelection(project, c) : null;
}
if (selectedOpeningId) {
const o = (project.openings ?? []).find((x) => x.id === selectedOpeningId);
if (o) return openingSelection(project, o);