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:
+41
-2
@@ -609,6 +609,8 @@ export default function App() {
|
|||||||
const setSelectedColumnIds = useStore((s) => s.setSelectedColumnIds);
|
const setSelectedColumnIds = useStore((s) => s.setSelectedColumnIds);
|
||||||
const selectedColumnId =
|
const selectedColumnId =
|
||||||
selectedColumnIds.length === 1 ? selectedColumnIds[0] : null;
|
selectedColumnIds.length === 1 ? selectedColumnIds[0] : null;
|
||||||
|
const selectedContextObjectIds = useStore((s) => s.selectedContextObjectIds);
|
||||||
|
const setSelectedContextObjectIds = useStore((s) => s.setSelectedContextObjectIds);
|
||||||
|
|
||||||
// ── Kontextmenüs (transienter UI-State, bleibt lokal) ─────────────────────
|
// ── Kontextmenüs (transienter UI-State, bleibt lokal) ─────────────────────
|
||||||
// Eigenschaften-Zwischenablage des Ebenen-Kontextmenüs (Farbe + Linienstärke).
|
// Eigenschaften-Zwischenablage des Ebenen-Kontextmenüs (Farbe + Linienstärke).
|
||||||
@@ -1905,7 +1907,8 @@ export default function App() {
|
|||||||
selectedRoomIds.length ||
|
selectedRoomIds.length ||
|
||||||
selectedExtrudedSolidIds.length ||
|
selectedExtrudedSolidIds.length ||
|
||||||
selectedColumnIds.length ||
|
selectedColumnIds.length ||
|
||||||
selectedRoofIds.length
|
selectedRoofIds.length ||
|
||||||
|
selectedContextObjectIds.length
|
||||||
) {
|
) {
|
||||||
const dids = new Set(selectedDrawingIds);
|
const dids = new Set(selectedDrawingIds);
|
||||||
const wids = new Set(selectedWallIds);
|
const wids = new Set(selectedWallIds);
|
||||||
@@ -1916,6 +1919,7 @@ export default function App() {
|
|||||||
const exids = new Set(selectedExtrudedSolidIds);
|
const exids = new Set(selectedExtrudedSolidIds);
|
||||||
const colids = new Set(selectedColumnIds);
|
const colids = new Set(selectedColumnIds);
|
||||||
const rfids = new Set(selectedRoofIds);
|
const rfids = new Set(selectedRoofIds);
|
||||||
|
const ctxids = new Set(selectedContextObjectIds);
|
||||||
setProject((p) => ({
|
setProject((p) => ({
|
||||||
...p,
|
...p,
|
||||||
drawings2d: p.drawings2d.filter((d) => !dids.has(d.id)),
|
drawings2d: p.drawings2d.filter((d) => !dids.has(d.id)),
|
||||||
@@ -1931,6 +1935,7 @@ export default function App() {
|
|||||||
openings: (p.openings ?? []).filter(
|
openings: (p.openings ?? []).filter(
|
||||||
(o) => !oids.has(o.id) && !wids.has(o.hostWallId),
|
(o) => !oids.has(o.id) && !wids.has(o.hostWallId),
|
||||||
),
|
),
|
||||||
|
context: (p.context ?? []).filter((o) => !ctxids.has(o.id)),
|
||||||
}));
|
}));
|
||||||
setSelectedDrawingIds([]);
|
setSelectedDrawingIds([]);
|
||||||
setSelectedWallIds([]);
|
setSelectedWallIds([]);
|
||||||
@@ -1941,6 +1946,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -1958,6 +1964,7 @@ export default function App() {
|
|||||||
selectedExtrudedSolidIds,
|
selectedExtrudedSolidIds,
|
||||||
selectedColumnIds,
|
selectedColumnIds,
|
||||||
selectedRoofIds,
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
selectedSectionLineId,
|
selectedSectionLineId,
|
||||||
project.walls,
|
project.walls,
|
||||||
project.drawings2d,
|
project.drawings2d,
|
||||||
@@ -3531,6 +3538,7 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
// ── Site-/Kontext-Schicht ──────────────────────────────────────────────
|
// ── Site-/Kontext-Schicht ──────────────────────────────────────────────
|
||||||
contextObjects: project.context ?? [],
|
contextObjects: project.context ?? [],
|
||||||
|
selectedContextObjectIds,
|
||||||
onAddContextObjects: addContextObjectsAndFit,
|
onAddContextObjects: addContextObjectsAndFit,
|
||||||
onRemoveContextObject: removeContextObject,
|
onRemoveContextObject: removeContextObject,
|
||||||
onGenerateTerrain: generateTerrain,
|
onGenerateTerrain: generateTerrain,
|
||||||
@@ -3555,6 +3563,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "Wand":
|
case "Wand":
|
||||||
@@ -3932,6 +3941,7 @@ export default function App() {
|
|||||||
sectionLine,
|
sectionLine,
|
||||||
selectedSectionLineId,
|
selectedSectionLineId,
|
||||||
selectedViewSnapshotId,
|
selectedViewSnapshotId,
|
||||||
|
selectedContextObjectIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ── Ausschnitt-Anwahl: „bleibt bis zur Abweichung" (DOSSIER A2) ───────────
|
// ── Ausschnitt-Anwahl: „bleibt bis zur Abweichung" (DOSSIER A2) ───────────
|
||||||
@@ -4417,6 +4427,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
setStampEditorRoomId(roomId);
|
setStampEditorRoomId(roomId);
|
||||||
};
|
};
|
||||||
@@ -4433,6 +4444,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
setEditTextId(drawingId);
|
setEditTextId(drawingId);
|
||||||
};
|
};
|
||||||
@@ -4546,6 +4558,7 @@ export default function App() {
|
|||||||
if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]);
|
if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]);
|
||||||
if (keep !== "column") setSelectedColumnIds([]);
|
if (keep !== "column") setSelectedColumnIds([]);
|
||||||
if (keep !== "roof") setSelectedRoofIds([]);
|
if (keep !== "roof") setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
if (keep !== "sectionLine") setSelectedSectionLineId(null);
|
if (keep !== "sectionLine") setSelectedSectionLineId(null);
|
||||||
};
|
};
|
||||||
if (sel.sectionLineId) {
|
if (sel.sectionLineId) {
|
||||||
@@ -4596,6 +4609,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
};
|
};
|
||||||
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die
|
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die
|
||||||
@@ -4679,6 +4693,21 @@ export default function App() {
|
|||||||
// Leertreffer (kein Bauteil) → alle Selektionen leeren.
|
// Leertreffer (kein Bauteil) → alle Selektionen leeren.
|
||||||
const onViewport3dPick = (hit: Pick3dHit, additive: boolean) => {
|
const onViewport3dPick = (hit: Pick3dHit, additive: boolean) => {
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
|
setSelectedWallIds([]);
|
||||||
|
setSelectedCeilingIds([]);
|
||||||
|
setSelectedDrawingId(null);
|
||||||
|
setSelectedOpeningIds([]);
|
||||||
|
setSelectedStairIds([]);
|
||||||
|
setSelectedRoomIds([]);
|
||||||
|
setSelectedExtrudedSolidIds([]);
|
||||||
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
|
setSelectedSectionLineId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hit.kind === "contextObject") {
|
||||||
|
setSelectedContextObjectIds([hit.id]);
|
||||||
setSelectedWallIds([]);
|
setSelectedWallIds([]);
|
||||||
setSelectedCeilingIds([]);
|
setSelectedCeilingIds([]);
|
||||||
setSelectedDrawingId(null);
|
setSelectedDrawingId(null);
|
||||||
@@ -4704,6 +4733,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4717,6 +4747,7 @@ export default function App() {
|
|||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4730,6 +4761,7 @@ export default function App() {
|
|||||||
setSelectedRoomIds([]);
|
setSelectedRoomIds([]);
|
||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hit.kind === "wall") {
|
if (hit.kind === "wall") {
|
||||||
@@ -4746,6 +4778,7 @@ export default function App() {
|
|||||||
setSelectedDrawingId(null);
|
setSelectedDrawingId(null);
|
||||||
setSelectedOpeningIds([]);
|
setSelectedOpeningIds([]);
|
||||||
setSelectedStairIds([]);
|
setSelectedStairIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedRoomIds([]);
|
setSelectedRoomIds([]);
|
||||||
setSelectedExtrudedSolidIds([]);
|
setSelectedExtrudedSolidIds([]);
|
||||||
setSelectedColumnIds([]);
|
setSelectedColumnIds([]);
|
||||||
@@ -5046,6 +5079,7 @@ export default function App() {
|
|||||||
selectedColumnIds={selectedColumnIds}
|
selectedColumnIds={selectedColumnIds}
|
||||||
selectedRoomIds={selectedRoomIds}
|
selectedRoomIds={selectedRoomIds}
|
||||||
selectedRoofIds={selectedRoofIds}
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedContextObjectIds={selectedContextObjectIds}
|
||||||
selectedSectionLineId={selectedSectionLineId}
|
selectedSectionLineId={selectedSectionLineId}
|
||||||
roomStamp={roomStamp}
|
roomStamp={roomStamp}
|
||||||
onStampMove={moveRoomStamp}
|
onStampMove={moveRoomStamp}
|
||||||
@@ -6185,6 +6219,7 @@ function Content({
|
|||||||
selectedColumnIds,
|
selectedColumnIds,
|
||||||
selectedRoomIds,
|
selectedRoomIds,
|
||||||
selectedRoofIds,
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
selectedSectionLineId,
|
selectedSectionLineId,
|
||||||
roomStamp,
|
roomStamp,
|
||||||
onStampMove,
|
onStampMove,
|
||||||
@@ -6265,6 +6300,7 @@ function Content({
|
|||||||
selectedColumnIds: string[];
|
selectedColumnIds: string[];
|
||||||
selectedRoomIds: string[];
|
selectedRoomIds: string[];
|
||||||
selectedRoofIds: string[];
|
selectedRoofIds: string[];
|
||||||
|
selectedContextObjectIds: string[];
|
||||||
selectedSectionLineId: string | null;
|
selectedSectionLineId: string | null;
|
||||||
roomStamp: { roomId: string; anchor: Vec2 } | null;
|
roomStamp: { roomId: string; anchor: Vec2 } | null;
|
||||||
onStampMove: (roomId: string, delta: Vec2) => void;
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
@@ -6337,7 +6373,8 @@ function Content({
|
|||||||
selectedCeilingIds.length === 0 &&
|
selectedCeilingIds.length === 0 &&
|
||||||
selectedOpeningIds.length === 0 &&
|
selectedOpeningIds.length === 0 &&
|
||||||
selectedStairIds.length === 0 &&
|
selectedStairIds.length === 0 &&
|
||||||
selectedRoofIds.length === 0
|
selectedRoofIds.length === 0 &&
|
||||||
|
selectedContextObjectIds.length === 0
|
||||||
? null
|
? null
|
||||||
: selectionHighlightLines(
|
: selectionHighlightLines(
|
||||||
project,
|
project,
|
||||||
@@ -6347,6 +6384,7 @@ function Content({
|
|||||||
selectedOpeningIds,
|
selectedOpeningIds,
|
||||||
selectedStairIds,
|
selectedStairIds,
|
||||||
selectedRoofIds,
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
project,
|
project,
|
||||||
@@ -6355,6 +6393,7 @@ function Content({
|
|||||||
selectedOpeningIds,
|
selectedOpeningIds,
|
||||||
selectedStairIds,
|
selectedStairIds,
|
||||||
selectedRoofIds,
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,13 @@ export function SitePanel() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="site-list">
|
<ul className="site-list">
|
||||||
{objects.map((obj) => (
|
{objects.map((obj) => (
|
||||||
<li key={obj.id} className="site-row">
|
<li
|
||||||
|
key={obj.id}
|
||||||
|
className={
|
||||||
|
"site-row" +
|
||||||
|
(host.selectedContextObjectIds.includes(obj.id) ? " site-row-selected" : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
<span className="site-name" title={obj.name}>
|
<span className="site-name" title={obj.name}>
|
||||||
{obj.name}
|
{obj.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -402,6 +402,8 @@ export interface PanelHostValue {
|
|||||||
// ── Site-/Kontext-Schicht (SitePanel) ───────────────────────────────────
|
// ── Site-/Kontext-Schicht (SitePanel) ───────────────────────────────────
|
||||||
/** Aktuelle Kontext-Objekte (Meshes/Konturen/Gelände) aus `project.context`. */
|
/** Aktuelle Kontext-Objekte (Meshes/Konturen/Gelände) aus `project.context`. */
|
||||||
contextObjects: ContextObject[];
|
contextObjects: ContextObject[];
|
||||||
|
/** Im 3D-Viewport angewählte Kontext-Objekte (Umriss-Hervorhebung), s. selectionSlice. */
|
||||||
|
selectedContextObjectIds: string[];
|
||||||
/** Fügt mehrere Kontext-Objekte hinzu (z. B. aus einem DXF-Import). */
|
/** Fügt mehrere Kontext-Objekte hinzu (z. B. aus einem DXF-Import). */
|
||||||
onAddContextObjects: (objs: ContextObject[]) => void;
|
onAddContextObjects: (objs: ContextObject[]) => void;
|
||||||
/** Entfernt ein Kontext-Objekt per ID. */
|
/** Entfernt ein Kontext-Objekt per ID. */
|
||||||
|
|||||||
@@ -101,6 +101,48 @@ describe("selectionHighlightLines", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("selectionHighlightLines — Kontext-Objekte", () => {
|
||||||
|
// Ein einzelnes Dreieck als Kontext-Mesh (importiertes Gebäude): das
|
||||||
|
// Highlight zeichnet dessen achsenparallele Bounding-Box (12 Kanten), NICHT
|
||||||
|
// das volle Dreiecks-Wireframe (s. Kommentar in toWalls3d.ts).
|
||||||
|
const withContext: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
id: "CTX1",
|
||||||
|
type: "importedMesh",
|
||||||
|
name: "Testgebäude",
|
||||||
|
positions: [0, 0, 0, 2, 0, 0, 2, 3, 4],
|
||||||
|
indices: [0, 1, 2],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("leere Kontext-Auswahl -> leeres Array", () => {
|
||||||
|
const lines = selectionHighlightLines(withContext, [], [], RGB, [], [], [], []);
|
||||||
|
expect(lines.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unbekannte Kontext-Id -> leeres Array", () => {
|
||||||
|
const lines = selectionHighlightLines(withContext, [], [], RGB, [], [], [], ["does-not-exist"]);
|
||||||
|
expect(lines.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ein Kontext-Objekt -> Bounding-Box-Umriss (12 Kanten)", () => {
|
||||||
|
const lines = selectionHighlightLines(withContext, [], [], RGB, [], [], [], ["CTX1"]);
|
||||||
|
expect(lines.length).toBe(12 * VERTICES_PER_EDGE * FLOATS_PER_VERTEX);
|
||||||
|
expect(lines[3]).toBeCloseTo(RGB[0], 6);
|
||||||
|
expect(lines[5]).toBeCloseTo(RGB[2], 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Kontext-Objekt zusammen mit einer Wand -> Summe der Einzelgeometrien", () => {
|
||||||
|
const wallOnly = selectionHighlightLines(withContext, ["W2"], [], RGB);
|
||||||
|
const ctxOnly = selectionHighlightLines(withContext, [], [], RGB, [], [], [], ["CTX1"]);
|
||||||
|
const both = selectionHighlightLines(withContext, ["W2"], [], RGB, [], [], [], ["CTX1"]);
|
||||||
|
expect(both.length).toBe(wallOnly.length + ctxOnly.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Öffnungen als echte Löcher in EINEM Körper (3D-Pfad, layeredWalls:true)", () => {
|
describe("Öffnungen als echte Löcher in EINEM Körper (3D-Pfad, layeredWalls:true)", () => {
|
||||||
// Wand W1 (EG, Wandtyp "aw" = 4 Schichten) mit ZWEI Fenstern, deren Achsen-
|
// Wand W1 (EG, Wandtyp "aw" = 4 Schichten) mit ZWEI Fenstern, deren Achsen-
|
||||||
// Intervalle sich ÜBERLAPPEN, aber auf UNTERSCHIEDLICHER Höhe liegen:
|
// Intervalle sich ÜBERLAPPEN, aber auf UNTERSCHIEDLICHER Höhe liegen:
|
||||||
@@ -1890,3 +1932,52 @@ describe("pickGeometry — Dächer (Ray-Dreieck-Pick)", () => {
|
|||||||
for (const tri of roofs[0].tris) for (const p of tri) expect(p[1]).toBeGreaterThan(0);
|
for (const tri of roofs[0].tris) for (const p of tri) expect(p[1]).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("pickGeometry — Kontext-Meshes (importierte Gebäude/Terrain)", () => {
|
||||||
|
const withContext: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
id: "CTX1",
|
||||||
|
type: "importedMesh",
|
||||||
|
name: "Testgebäude",
|
||||||
|
// model (x,y,z=Höhe) -> world (x, z, y): ein Dreieck.
|
||||||
|
positions: [0, 0, 0, 2, 0, 0, 2, 3, 4],
|
||||||
|
indices: [0, 1, 2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CONTOURS1",
|
||||||
|
type: "contourSet",
|
||||||
|
name: "Höhenlinien",
|
||||||
|
contours: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("liefert je Kontext-Objekt World-Dreiecke, contourSet wird übersprungen", () => {
|
||||||
|
const { contextMeshes } = pickGeometry(withContext);
|
||||||
|
expect(contextMeshes.length).toBe(1);
|
||||||
|
expect(contextMeshes[0].contextObjectId).toBe("CTX1");
|
||||||
|
expect(contextMeshes[0].tris.length).toBe(1);
|
||||||
|
// world = (x, z, y) — dasselbe Dreieck (0,0,0),(2,0,0),(2,3,4)
|
||||||
|
// -> world (0,0,0),(2,0,0),(2,4,3).
|
||||||
|
expect(contextMeshes[0].tris[0][2]).toEqual([2, 4, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kaputte Indizes werden robust übersprungen statt abzustürzen", () => {
|
||||||
|
const broken: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
id: "CTX2",
|
||||||
|
type: "importedMesh",
|
||||||
|
name: "Kaputt",
|
||||||
|
positions: [0, 0, 0, 1, 0, 0, 1, 1, 0],
|
||||||
|
indices: [0, 1, 99], // Index 99 existiert nicht
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const { contextMeshes } = pickGeometry(broken);
|
||||||
|
expect(contextMeshes.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+57
-3
@@ -62,7 +62,7 @@ import type { WallCuts } from "../model/joins";
|
|||||||
import { add, dot, leftNormal, lineIntersect, normalize, scale, sub } from "../model/geometry";
|
import { add, dot, leftNormal, lineIntersect, normalize, scale, sub } from "../model/geometry";
|
||||||
import type { Line } from "../model/geometry";
|
import type { Line } from "../model/geometry";
|
||||||
import { resolveHatch } from "./generatePlan";
|
import { resolveHatch } from "./generatePlan";
|
||||||
import type { PickOpening, PickRoof, PickStair } from "../viewport/raycast3d";
|
import type { PickOpening, PickRoof, PickStair, PickContextMesh } from "../viewport/raycast3d";
|
||||||
|
|
||||||
/** World-Punkt [x, Höhe, y] für den Dach-Pick (Vec3-Konvention von raycast3d). */
|
/** World-Punkt [x, Höhe, y] für den Dach-Pick (Vec3-Konvention von raycast3d). */
|
||||||
type RoofPickVec3 = [number, number, number];
|
type RoofPickVec3 = [number, number, number];
|
||||||
@@ -2825,6 +2825,7 @@ export function pickGeometry(
|
|||||||
openings: PickOpening[];
|
openings: PickOpening[];
|
||||||
stairs: PickStair[];
|
stairs: PickStair[];
|
||||||
roofs: PickRoof[];
|
roofs: PickRoof[];
|
||||||
|
contextMeshes: PickContextMesh[];
|
||||||
} {
|
} {
|
||||||
// Dächer: Dachflächen + Giebel als WORLD-Dreiecke (fan-trianguliert wie
|
// Dächer: Dachflächen + Giebel als WORLD-Dreiecke (fan-trianguliert wie
|
||||||
// emitRoofs; model [x,y,z=Höhe] → world [x, z, y]) — präzise geneigte
|
// emitRoofs; model [x,y,z=Höhe] → world [x, z, y]) — präzise geneigte
|
||||||
@@ -2878,7 +2879,27 @@ export function pickGeometry(
|
|||||||
stairId: st.id,
|
stairId: st.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { walls: projectToWalls3d(project), slabs: emitSlabs(project), openings, stairs, roofs };
|
// Kontext-Meshes (importierte Gebäude/Terrain, s. ContextObject): rohe
|
||||||
|
// WORLD-Dreiecke wie beim Dach, direkt aus dem Indexpuffer (kein Fan nötig —
|
||||||
|
// ImportedMesh/TerrainMesh liefern bereits ein echtes Dreiecksnetz). Kaputte
|
||||||
|
// Indizes robust überspringen statt abzustürzen (importierte Daten).
|
||||||
|
const contextMeshes: PickContextMesh[] = [];
|
||||||
|
for (const obj of project.context ?? []) {
|
||||||
|
if (obj.type === "contourSet") continue;
|
||||||
|
const pos = obj.positions;
|
||||||
|
const vcount = pos.length / 3;
|
||||||
|
const tris: [RoofPickVec3, RoofPickVec3, RoofPickVec3][] = [];
|
||||||
|
for (let i = 0; i + 2 < obj.indices.length; i += 3) {
|
||||||
|
const ia = obj.indices[i];
|
||||||
|
const ib = obj.indices[i + 1];
|
||||||
|
const ic = obj.indices[i + 2];
|
||||||
|
if (ia >= vcount || ib >= vcount || ic >= vcount) continue;
|
||||||
|
const at = (idx: number): RoofPickVec3 => [pos[idx * 3], pos[idx * 3 + 2], pos[idx * 3 + 1]];
|
||||||
|
tris.push([at(ia), at(ib), at(ic)]);
|
||||||
|
}
|
||||||
|
if (tris.length > 0) contextMeshes.push({ tris, contextObjectId: obj.id });
|
||||||
|
}
|
||||||
|
return { walls: projectToWalls3d(project), slabs: emitSlabs(project), openings, stairs, roofs, contextMeshes };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2915,13 +2936,15 @@ export function selectionHighlightLines(
|
|||||||
openingIds: string[] = [],
|
openingIds: string[] = [],
|
||||||
stairIds: string[] = [],
|
stairIds: string[] = [],
|
||||||
roofIds: string[] = [],
|
roofIds: string[] = [],
|
||||||
|
contextObjectIds: string[] = [],
|
||||||
): Float32Array {
|
): Float32Array {
|
||||||
if (
|
if (
|
||||||
wallIds.length === 0 &&
|
wallIds.length === 0 &&
|
||||||
ceilingIds.length === 0 &&
|
ceilingIds.length === 0 &&
|
||||||
openingIds.length === 0 &&
|
openingIds.length === 0 &&
|
||||||
stairIds.length === 0 &&
|
stairIds.length === 0 &&
|
||||||
roofIds.length === 0
|
roofIds.length === 0 &&
|
||||||
|
contextObjectIds.length === 0
|
||||||
) {
|
) {
|
||||||
return new Float32Array(0);
|
return new Float32Array(0);
|
||||||
}
|
}
|
||||||
@@ -2931,6 +2954,7 @@ export function selectionHighlightLines(
|
|||||||
const openingSet = new Set(openingIds);
|
const openingSet = new Set(openingIds);
|
||||||
const stairSet = new Set(stairIds);
|
const stairSet = new Set(stairIds);
|
||||||
const roofSet = new Set(roofIds);
|
const roofSet = new Set(roofIds);
|
||||||
|
const contextObjSet = new Set(contextObjectIds);
|
||||||
const out: number[] = [];
|
const out: number[] = [];
|
||||||
const pushEdge = (a: RVec3, b: RVec3) => {
|
const pushEdge = (a: RVec3, b: RVec3) => {
|
||||||
out.push(a[0], a[1], a[2], rgb[0], rgb[1], rgb[2]);
|
out.push(a[0], a[1], a[2], rgb[0], rgb[1], rgb[2]);
|
||||||
@@ -3013,5 +3037,35 @@ export function selectionHighlightLines(
|
|||||||
for (const gab of g.gables) polyEdges(gab);
|
for (const gab of g.gables) polyEdges(gab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Kontext-Objekt (importiertes Gebäude/Terrain): Umriss als achsenparallele
|
||||||
|
// Bounding-Box statt vollem Dreiecks-Wireframe — importierte Meshes haben oft
|
||||||
|
// tausende Dreiecke, deren Kanten einzeln zu zeichnen wäre unbrauchbar dicht.
|
||||||
|
if (contextObjSet.size > 0) {
|
||||||
|
for (const obj of project.context ?? []) {
|
||||||
|
if (obj.type === "contourSet" || !contextObjSet.has(obj.id)) continue;
|
||||||
|
const pos = obj.positions;
|
||||||
|
let minX = Infinity, maxX = -Infinity;
|
||||||
|
let minY = Infinity, maxY = -Infinity;
|
||||||
|
let minH = Infinity, maxH = -Infinity;
|
||||||
|
for (let i = 0; i + 2 < pos.length; i += 3) {
|
||||||
|
const x = pos[i], y = pos[i + 1], h = pos[i + 2];
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
if (h < minH) minH = h;
|
||||||
|
if (h > maxH) maxH = h;
|
||||||
|
}
|
||||||
|
if (!isFinite(minX)) continue;
|
||||||
|
const corners: RVec3[] = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const x = i & 1 ? maxX : minX;
|
||||||
|
const h = i & 2 ? maxH : minH;
|
||||||
|
const y = i & 4 ? maxY : minY;
|
||||||
|
corners.push([x, h, y]);
|
||||||
|
}
|
||||||
|
for (const [i, j] of BOX_EDGES) pushEdge(corners[i], corners[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new Float32Array(out);
|
return new Float32Array(out);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface SelectionSlice {
|
|||||||
selectedColumnIds: string[];
|
selectedColumnIds: string[];
|
||||||
// Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen.
|
// Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen.
|
||||||
selectedRoofIds: string[];
|
selectedRoofIds: string[];
|
||||||
|
// Gewählte Kontext-Objekte (importierte Gebäude/Terrain) als MENGE von IDs.
|
||||||
|
selectedContextObjectIds: string[];
|
||||||
// Gewählte Schnitt-/Ansichtslinie (DrawingLevel-Id) — EINZELauswahl (eine Linie
|
// Gewählte Schnitt-/Ansichtslinie (DrawingLevel-Id) — EINZELauswahl (eine Linie
|
||||||
// zur Zeit), getrennt von den übrigen Kanälen. `null` = keine Linie gewählt.
|
// zur Zeit), getrennt von den übrigen Kanälen. `null` = keine Linie gewählt.
|
||||||
selectedSectionLineId: string | null;
|
selectedSectionLineId: string | null;
|
||||||
@@ -38,6 +40,7 @@ export interface SelectionSlice {
|
|||||||
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
||||||
setSelectedColumnIds: (ids: string[]) => void;
|
setSelectedColumnIds: (ids: string[]) => void;
|
||||||
setSelectedRoofIds: (ids: string[]) => void;
|
setSelectedRoofIds: (ids: string[]) => void;
|
||||||
|
setSelectedContextObjectIds: (ids: string[]) => void;
|
||||||
setSelectedSectionLineId: (id: string | null) => void;
|
setSelectedSectionLineId: (id: string | null) => void;
|
||||||
/** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */
|
/** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
@@ -57,6 +60,7 @@ export function createSelectionSlice(
|
|||||||
selectedExtrudedSolidIds: [],
|
selectedExtrudedSolidIds: [],
|
||||||
selectedColumnIds: [],
|
selectedColumnIds: [],
|
||||||
selectedRoofIds: [],
|
selectedRoofIds: [],
|
||||||
|
selectedContextObjectIds: [],
|
||||||
selectedSectionLineId: null,
|
selectedSectionLineId: null,
|
||||||
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
|
||||||
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
|
||||||
@@ -67,6 +71,7 @@ export function createSelectionSlice(
|
|||||||
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
|
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
|
||||||
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
|
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
|
||||||
setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }),
|
setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }),
|
||||||
|
setSelectedContextObjectIds: (ids) => set({ selectedContextObjectIds: ids }),
|
||||||
setSelectedSectionLineId: (id) => set({ selectedSectionLineId: id }),
|
setSelectedSectionLineId: (id) => set({ selectedSectionLineId: id }),
|
||||||
clearSelection: () =>
|
clearSelection: () =>
|
||||||
set({
|
set({
|
||||||
@@ -79,6 +84,7 @@ export function createSelectionSlice(
|
|||||||
selectedExtrudedSolidIds: [],
|
selectedExtrudedSolidIds: [],
|
||||||
selectedColumnIds: [],
|
selectedColumnIds: [],
|
||||||
selectedRoofIds: [],
|
selectedRoofIds: [],
|
||||||
|
selectedContextObjectIds: [],
|
||||||
selectedSectionLineId: null,
|
selectedSectionLineId: null,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4043,6 +4043,10 @@ body {
|
|||||||
.site-panel .site-row:hover {
|
.site-panel .site-row:hover {
|
||||||
background: var(--accent-dim);
|
background: var(--accent-dim);
|
||||||
}
|
}
|
||||||
|
.site-panel .site-row-selected {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
outline: 1px solid var(--accent);
|
||||||
|
}
|
||||||
.site-panel .site-name {
|
.site-panel .site-name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import { drawingGripVertices, drawingMoveAnchor } from "./drawingGrips";
|
|||||||
|
|
||||||
/** Ein per 3D-Klick getroffenes Bauteil, oder null (Leerraum). */
|
/** Ein per 3D-Klick getroffenes Bauteil, oder null (Leerraum). */
|
||||||
export type Pick3dHit =
|
export type Pick3dHit =
|
||||||
| { kind: "wall" | "ceiling" | "opening" | "stair" | "roof"; id: string }
|
| { kind: "wall" | "ceiling" | "opening" | "stair" | "roof" | "contextObject"; id: string }
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
/** Klick-vs-Drag-Schwelle (Pixel): bleibt die Maus zwischen pointerdown und
|
/** Klick-vs-Drag-Schwelle (Pixel): bleibt die Maus zwischen pointerdown und
|
||||||
@@ -798,10 +798,10 @@ export function Wasm3DViewport({
|
|||||||
const ndc = toNdc(clientX, clientY);
|
const ndc = toNdc(clientX, clientY);
|
||||||
if (!cb || !o || !ndc) return;
|
if (!cb || !o || !ndc) return;
|
||||||
const ray = cameraRay(orbitCamera(o), ndc.ndcX, ndc.ndcY, ndc.aspect);
|
const ray = cameraRay(orbitCamera(o), ndc.ndcX, ndc.ndcY, ndc.aspect);
|
||||||
const { walls, slabs, openings, stairs, roofs } = pickGeometry(projectRef.current);
|
const { walls, slabs, openings, stairs, roofs, contextMeshes } = pickGeometry(projectRef.current);
|
||||||
// Erweiterter Pick inkl. Öffnungen (Fenster/Türen) + Treppen — sonst wären
|
// Erweiterter Pick inkl. Öffnungen (Fenster/Türen) + Treppen + Kontext-
|
||||||
// im WASM-Viewport nur Wände/Decken anwählbar.
|
// Meshes — sonst wären im WASM-Viewport nur Wände/Decken anwählbar.
|
||||||
const hit = pickNearest(ray, walls, slabs, openings, stairs, roofs);
|
const hit = pickNearest(ray, walls, slabs, openings, stairs, roofs, contextMeshes);
|
||||||
cb(hit ? { kind: hit.kind, id: hit.id } : null, additive);
|
cb(hit ? { kind: hit.kind, id: hit.id } : null, additive);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
type PickSlab,
|
type PickSlab,
|
||||||
type PickOpening,
|
type PickOpening,
|
||||||
type PickStair,
|
type PickStair,
|
||||||
|
type PickContextMesh,
|
||||||
} from "./raycast3d";
|
} from "./raycast3d";
|
||||||
|
|
||||||
// Kamera bei (0,1,10), Blick entlang -Z auf (0,1,0). Zentraler Strahl (ndc 0,0)
|
// 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");
|
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;
|
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. */
|
/** Ergebnis eines Picks: getroffenes Bauteil + Distanz entlang des Strahls. */
|
||||||
export interface PickHit {
|
export interface PickHit {
|
||||||
kind: "wall" | "ceiling" | "opening" | "stair" | "roof";
|
kind: "wall" | "ceiling" | "opening" | "stair" | "roof" | "contextObject";
|
||||||
id: string;
|
id: string;
|
||||||
t: number;
|
t: number;
|
||||||
/** Bei `kind:"opening"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
/** Bei `kind:"opening"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||||
openingId?: string;
|
openingId?: string;
|
||||||
/** Bei `kind:"stair"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
/** Bei `kind:"stair"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||||
stairId?: string;
|
stairId?: string;
|
||||||
|
/** Bei `kind:"contextObject"` dieselbe Id wie `id` (ausdrucksstärkerer Zugriff). */
|
||||||
|
contextObjectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EPS = 1e-9;
|
const EPS = 1e-9;
|
||||||
@@ -362,6 +376,17 @@ function rayRoofTris(ray: Ray, roof: PickRoof): number | null {
|
|||||||
return best;
|
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
|
// Overload OHNE Öffnungen/Treppen: bleibt exakt die bisherige Signatur, sodass
|
||||||
// der bestehende Aufrufer (Wasm3DViewport.tsx: `pickNearest(ray, walls, slabs)`)
|
// der bestehende Aufrufer (Wasm3DViewport.tsx: `pickNearest(ray, walls, slabs)`)
|
||||||
// unverändert kompiliert — inklusive des dort erwarteten engen `kind`-Typs
|
// unverändert kompiliert — inklusive des dort erwarteten engen `kind`-Typs
|
||||||
@@ -372,7 +397,8 @@ export function pickNearest(
|
|||||||
walls: readonly PickWall[],
|
walls: readonly PickWall[],
|
||||||
slabs: readonly PickSlab[],
|
slabs: readonly PickSlab[],
|
||||||
): (PickHit & { kind: "wall" | "ceiling" }) | null;
|
): (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(
|
export function pickNearest(
|
||||||
ray: Ray,
|
ray: Ray,
|
||||||
walls: readonly PickWall[],
|
walls: readonly PickWall[],
|
||||||
@@ -380,6 +406,7 @@ export function pickNearest(
|
|||||||
openings: readonly PickOpening[],
|
openings: readonly PickOpening[],
|
||||||
stairs?: readonly PickStair[],
|
stairs?: readonly PickStair[],
|
||||||
roofs?: readonly PickRoof[],
|
roofs?: readonly PickRoof[],
|
||||||
|
contextMeshes?: readonly PickContextMesh[],
|
||||||
): PickHit | null;
|
): PickHit | null;
|
||||||
/**
|
/**
|
||||||
* Nächstes getroffenes Bauteil (kleinstes t) über Wände, Decken, Öffnungen und
|
* Nächstes getroffenes Bauteil (kleinstes t) über Wände, Decken, Öffnungen und
|
||||||
@@ -397,6 +424,7 @@ export function pickNearest(
|
|||||||
openings: readonly PickOpening[] = [],
|
openings: readonly PickOpening[] = [],
|
||||||
stairs: readonly PickStair[] = [],
|
stairs: readonly PickStair[] = [],
|
||||||
roofs: readonly PickRoof[] = [],
|
roofs: readonly PickRoof[] = [],
|
||||||
|
contextMeshes: readonly PickContextMesh[] = [],
|
||||||
): PickHit | null {
|
): PickHit | null {
|
||||||
const TIE_EPS = 1e-6;
|
const TIE_EPS = 1e-6;
|
||||||
let hit: PickHit | null = null;
|
let hit: PickHit | null = null;
|
||||||
@@ -433,6 +461,13 @@ export function pickNearest(
|
|||||||
const t = rayRoofTris(ray, r);
|
const t = rayRoofTris(ray, r);
|
||||||
if (t !== null) consider({ kind: "roof", id: r.roofId, t }, 1);
|
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;
|
return hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user