Kontext-Objekte im 3D-Viewport anwählbar (Klick, Bounding-Box-Highlight, Löschen)
Importierte Gebäude/Terrain-Meshes waren bisher nur ein Eintrag im Geo-Panel, nicht direkt im 3D anwählbar. Raycast/Pick-Geometrie um Kontext-Meshes erweitert, Auswahl-Kanal (selectedContextObjectIds) ergänzt, Highlight als Bounding-Box-Umriss (volles Dreiecks-Wireframe wäre bei grossen Importen zu dicht), Entf-Taste löscht die Auswahl, SitePanel hebt sie in der Liste hervor.
This commit is contained in:
@@ -57,7 +57,7 @@ import { drawingGripVertices, drawingMoveAnchor } from "./drawingGrips";
|
||||
|
||||
/** Ein per 3D-Klick getroffenes Bauteil, oder null (Leerraum). */
|
||||
export type Pick3dHit =
|
||||
| { kind: "wall" | "ceiling" | "opening" | "stair" | "roof"; id: string }
|
||||
| { kind: "wall" | "ceiling" | "opening" | "stair" | "roof" | "contextObject"; id: string }
|
||||
| null;
|
||||
|
||||
/** Klick-vs-Drag-Schwelle (Pixel): bleibt die Maus zwischen pointerdown und
|
||||
@@ -798,10 +798,10 @@ export function Wasm3DViewport({
|
||||
const ndc = toNdc(clientX, clientY);
|
||||
if (!cb || !o || !ndc) return;
|
||||
const ray = cameraRay(orbitCamera(o), ndc.ndcX, ndc.ndcY, ndc.aspect);
|
||||
const { walls, slabs, openings, stairs, roofs } = pickGeometry(projectRef.current);
|
||||
// Erweiterter Pick inkl. Öffnungen (Fenster/Türen) + Treppen — sonst wären
|
||||
// im WASM-Viewport nur Wände/Decken anwählbar.
|
||||
const hit = pickNearest(ray, walls, slabs, openings, stairs, roofs);
|
||||
const { walls, slabs, openings, stairs, roofs, contextMeshes } = pickGeometry(projectRef.current);
|
||||
// Erweiterter Pick inkl. Öffnungen (Fenster/Türen) + Treppen + Kontext-
|
||||
// Meshes — sonst wären im WASM-Viewport nur Wände/Decken anwählbar.
|
||||
const hit = pickNearest(ray, walls, slabs, openings, stairs, roofs, contextMeshes);
|
||||
cb(hit ? { kind: hit.kind, id: hit.id } : null, additive);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type PickSlab,
|
||||
type PickOpening,
|
||||
type PickStair,
|
||||
type PickContextMesh,
|
||||
} from "./raycast3d";
|
||||
|
||||
// Kamera bei (0,1,10), Blick entlang -Z auf (0,1,0). Zentraler Strahl (ndc 0,0)
|
||||
@@ -207,3 +208,69 @@ describe("pickNearest — Öffnungen und Treppen", () => {
|
||||
expect(hit!.id).toBe("w1");
|
||||
});
|
||||
});
|
||||
|
||||
// Kontext-Mesh (importiertes Gebäude/Terrain): eine flache, senkrechte
|
||||
// Rechteckfläche bei model y=5 (world z=5), model x∈[-1,1], model z (Höhe)
|
||||
// ∈[0,2] — dieselbe Front wie WALL, aber als rohes Dreiecksnetz statt Quader.
|
||||
const CONTEXT_MESH: PickContextMesh = {
|
||||
tris: [
|
||||
[
|
||||
[-1, 0, 5],
|
||||
[1, 0, 5],
|
||||
[1, 2, 5],
|
||||
],
|
||||
[
|
||||
[-1, 0, 5],
|
||||
[1, 2, 5],
|
||||
[-1, 2, 5],
|
||||
],
|
||||
],
|
||||
contextObjectId: "ctx1",
|
||||
};
|
||||
|
||||
describe("pickNearest — Kontext-Meshes", () => {
|
||||
it("Strahl auf ein Kontext-Mesh liefert contextObjectId", () => {
|
||||
const hit = pickNearest(
|
||||
{ origin: [0, 1, 10], dir: [0, 0, -1] },
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[CONTEXT_MESH],
|
||||
);
|
||||
expect(hit).not.toBeNull();
|
||||
expect(hit!.kind).toBe("contextObject");
|
||||
expect(hit!.contextObjectId).toBe("ctx1");
|
||||
expect(hit!.t).toBeCloseTo(5, 6);
|
||||
});
|
||||
|
||||
it("Strahl neben dem Mesh verfehlt", () => {
|
||||
const hit = pickNearest(
|
||||
{ origin: [5, 1, 10], dir: [0, 0, -1] },
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[CONTEXT_MESH],
|
||||
);
|
||||
expect(hit).toBeNull();
|
||||
});
|
||||
|
||||
it("bei näherer Wand gewinnt die Wand vor dem weiter entfernten Kontext-Mesh", () => {
|
||||
// WALL-Front bei z=5.1 → t=4.9, näher als das Mesh bei z=5 (t=5).
|
||||
const hit = pickNearest(
|
||||
{ origin: [0, 1, 10], dir: [0, 0, -1] },
|
||||
[WALL],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[CONTEXT_MESH],
|
||||
);
|
||||
expect(hit).not.toBeNull();
|
||||
expect(hit!.kind).toBe("wall");
|
||||
expect(hit!.id).toBe("w1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,15 +96,29 @@ export interface PickRoof {
|
||||
roofId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal-Form eines Kontext-Meshes (importiertes Gebäude/Terrain, s.
|
||||
* `ContextObject`) für den Raycast: rohe WORLD-Dreiecke (dieselbe Konvention
|
||||
* wie {@link PickRoof} — model (x,y,z=Höhe) → world (x, z, y)), keine
|
||||
* Vereinfachung auf ein Bounding-Prisma (Gebäude-Meshes sind zu unregelmässig
|
||||
* dafür, ein grosszügiges Prisma stähle Klicks auf dahinterliegende Bauteile).
|
||||
*/
|
||||
export interface PickContextMesh {
|
||||
tris: ReadonlyArray<readonly [Vec3, Vec3, Vec3]>;
|
||||
contextObjectId: string;
|
||||
}
|
||||
|
||||
/** Ergebnis eines Picks: getroffenes Bauteil + Distanz entlang des Strahls. */
|
||||
export interface PickHit {
|
||||
kind: "wall" | "ceiling" | "opening" | "stair" | "roof";
|
||||
kind: "wall" | "ceiling" | "opening" | "stair" | "roof" | "contextObject";
|
||||
id: string;
|
||||
t: number;
|
||||
/** Bei `kind:"opening"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||
openingId?: string;
|
||||
/** Bei `kind:"stair"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||
stairId?: string;
|
||||
/** Bei `kind:"contextObject"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||
contextObjectId?: string;
|
||||
}
|
||||
|
||||
const EPS = 1e-9;
|
||||
@@ -362,6 +376,17 @@ function rayRoofTris(ray: Ray, roof: PickRoof): number | null {
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Kleinstes t über alle Dreiecke eines Kontext-Meshes, oder null — wie
|
||||
* {@link rayRoofTris}, dieselbe doppelseitige {@link rayTriangle}. */
|
||||
function rayContextMeshTris(ray: Ray, mesh: PickContextMesh): number | null {
|
||||
let best: number | null = null;
|
||||
for (const [a, b, c] of mesh.tris) {
|
||||
const t = rayTriangle(ray, a, b, c);
|
||||
if (t !== null && (best === null || t < best)) best = t;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Overload OHNE Öffnungen/Treppen: bleibt exakt die bisherige Signatur, sodass
|
||||
// der bestehende Aufrufer (Wasm3DViewport.tsx: `pickNearest(ray, walls, slabs)`)
|
||||
// unverändert kompiliert — inklusive des dort erwarteten engen `kind`-Typs
|
||||
@@ -372,7 +397,8 @@ export function pickNearest(
|
||||
walls: readonly PickWall[],
|
||||
slabs: readonly PickSlab[],
|
||||
): (PickHit & { kind: "wall" | "ceiling" }) | null;
|
||||
// Erweiterter Overload MIT Öffnungen/Treppen/Dächern: `kind` deckt alle Arten ab.
|
||||
// Erweiterter Overload MIT Öffnungen/Treppen/Dächern/Kontext-Meshes: `kind`
|
||||
// deckt alle Arten ab.
|
||||
export function pickNearest(
|
||||
ray: Ray,
|
||||
walls: readonly PickWall[],
|
||||
@@ -380,6 +406,7 @@ export function pickNearest(
|
||||
openings: readonly PickOpening[],
|
||||
stairs?: readonly PickStair[],
|
||||
roofs?: readonly PickRoof[],
|
||||
contextMeshes?: readonly PickContextMesh[],
|
||||
): PickHit | null;
|
||||
/**
|
||||
* Nächstes getroffenes Bauteil (kleinstes t) über Wände, Decken, Öffnungen und
|
||||
@@ -397,6 +424,7 @@ export function pickNearest(
|
||||
openings: readonly PickOpening[] = [],
|
||||
stairs: readonly PickStair[] = [],
|
||||
roofs: readonly PickRoof[] = [],
|
||||
contextMeshes: readonly PickContextMesh[] = [],
|
||||
): PickHit | null {
|
||||
const TIE_EPS = 1e-6;
|
||||
let hit: PickHit | null = null;
|
||||
@@ -433,6 +461,13 @@ export function pickNearest(
|
||||
const t = rayRoofTris(ray, r);
|
||||
if (t !== null) consider({ kind: "roof", id: r.roofId, t }, 1);
|
||||
}
|
||||
// Kontext-Meshes (importierte Gebäude/Terrain): Rang wie Wand/Decke/Dach.
|
||||
for (const m of contextMeshes) {
|
||||
const t = rayContextMeshTris(ray, m);
|
||||
if (t !== null) {
|
||||
consider({ kind: "contextObject", id: m.contextObjectId, t, contextObjectId: m.contextObjectId }, 1);
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user