Nordstern-3D: Klick-Auswahl von Bauteilen (Raycast)

Links-Klick auf eine Wand/Decke im 3D-Viewport selektiert das Bauteil
(TS-Raycast gegen OBB der Wände + extrudierte Deckenprismen), setzt die
Store-Selektion (setSelectedWallIds/setSelectedCeilingIds, Shift/Ctrl =
additiv) → Attribute- und Objekt-Info-Panel zeigen das Bauteil. Erster
Schritt zur 3D-Editierbarkeit.

- raycast3d.ts: reine Kamera-Strahl-/Schnitt-Mathematik (10 Unit-Tests).
- toWalls3d.ts: wallId/ceilingId in die 3D-Records + pickGeometry();
  Serde-Pfad unberührt (keine deny_unknown_fields, Extrafelder werden
  Rust-seitig ignoriert).
- Klick-vs-Drag-Schwelle (4px) trennt Auswahl von Orbit; Leertreffer
  leert die Auswahl.
This commit is contained in:
2026-07-04 06:17:45 +02:00
parent 8f4fac633f
commit 16d32223a4
6 changed files with 547 additions and 15 deletions
+33 -5
View File
@@ -49,6 +49,13 @@ export interface RWall {
color: RRgb;
/** Aufgelöster Schichtaufbau (leer = kein WallType auflösbar, siehe emitWall). */
layers: RLayer[];
/**
* Id der Ursprungs-Wand (`project.walls[].id`). Alle Öffnungs-Teilquader einer
* Wand tragen dieselbe Id. Rein für den TS-seitigen Pick-Raycast (raycast3d);
* der render3d/Serde-Pfad ignoriert das Feld (kein `deny_unknown_fields`,
* serde verwirft unbekannte Felder).
*/
wallId: string;
}
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
@@ -57,6 +64,12 @@ export interface RSlab {
zBottom: number;
zTop: number;
color: RRgb;
/**
* Id der Ursprungs-Decke (`project.ceilings[].id`). Nur für den TS-seitigen
* Pick-Raycast (raycast3d); der render3d/Serde-Pfad ignoriert das Feld
* (kein `deny_unknown_fields`).
*/
ceilingId: string;
}
/** Art eines rohen Kontext-Meshes (bestimmt in render3d die Default-Einfärbung). */
@@ -122,6 +135,7 @@ function pushSegment(
zTop: number,
thickness: number,
layers: RLayer[],
wallId: string,
): void {
if (to - from <= EPS) return;
if (zTop - zBottom <= EPS) return;
@@ -141,6 +155,7 @@ function pushSegment(
baseElevation: zBottom,
color: WALL_RGB,
layers,
wallId,
});
}
@@ -190,7 +205,7 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
if (cutouts.length === 0) {
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers);
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers, wall.id);
return;
}
@@ -202,23 +217,23 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
const segFrom = Math.max(cursor, from);
if (from > cursor) {
// Voller Wandpfeiler bis zur Aussparung.
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers);
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers, wall.id);
}
if (to > segFrom) {
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
if (oBottom > zBottom + EPS) {
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers);
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers, wall.id);
}
// Sturz über der Öffnung (bis zum Wandkopf).
if (oTop < zTop - EPS) {
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers);
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers, wall.id);
}
}
cursor = Math.max(cursor, to);
}
// Restlicher Wandpfeiler bis zum Achsenende.
if (cursor < axisLen) {
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers);
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers, wall.id);
}
}
@@ -236,6 +251,7 @@ function emitSlabs(project: Project): RSlab[] {
zBottom,
zTop,
color: SLAB_RGB,
ceilingId: c.id,
});
}
return out;
@@ -283,3 +299,15 @@ export function projectToModel3d(project: Project): RModel3d {
meshes: emitMeshes(project),
};
}
/**
* Pick-Geometrie für den TS-seitigen 3D-Raycast (siehe viewport/raycast3d): die
* Wand-Teilquader (mit `wallId`) und Decken-Prismen (mit `ceilingId`) desselben
* geflachten Modells wie {@link projectToModel3d}, aber ohne die Kontext-Meshes
* (die sind für die Bauteil-Auswahl irrelevant). Reine TS-Nutzung — geht NICHT an
* render3d; die Records tragen dieselben Geometriefelder wie die Serde-Payload,
* plus die Ids fürs Picking.
*/
export function pickGeometry(project: Project): { walls: RWall[]; slabs: RSlab[] } {
return { walls: projectToWalls3d(project), slabs: emitSlabs(project) };
}