App.tsx entschlackt: Kontextmenüs nach src/menus/, Views nach src/views/
Reiner Umzug (verhaltensgleich, byte-identische Funktionskörper): Kontextmenü- Builder (buildMenuItems + layer/level/plan/viewport-Menüs) nach src/menus/contextMenu.tsx; InlineEditor sowie Content/LevelPlanView/ SectionPlanView (der View-Router der Hauptansicht) nach src/views/. App.tsx 7130 → 5917 Zeilen. Setzt die in CONVENTIONS.md/state-architecture.md vorgesehene, bisher nie umgesetzte Struktur endlich um (STATUS.md §4.3). Dazu ein unabhängiger CSS-Fix: .plan-selected (Auswahl-Hervorhebung im 2D-Plan) fehlte die fill-opacity, die zwei Regeln weiter unten bei .plan-marquee/ .tool-preview-fill schon gesetzt ist. Im Dunkel-Theme ist --accent-dim transparent (zufällig unauffällig), im neuen Standard-Hell-Theme aber fast deckend weiss — dadurch verdeckte die Auswahl die Schraffur der gewählten Wand/Decke/des Raums vollständig.
This commit is contained in:
+8
-1221
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,322 @@
|
|||||||
|
// Kontextmenü-Builder — baut die Menü-Einträge je Quelle (Ebene / Zeichnungs-
|
||||||
|
// ebene / Grundriss / 3D-Viewport). 1:1 nach docs/design/context-menu.md.
|
||||||
|
// JEDER Eintrag ist verdrahtet ODER klar deaktiviert mit Tooltip (`title`, nennt
|
||||||
|
// den Grund) — kein stiller No-Op.
|
||||||
|
//
|
||||||
|
// Extrahiert aus App.tsx (reine Funktionen, keine Hooks/kein Store-Zugriff — die
|
||||||
|
// Aktionen kommen als `MenuActions` von App herein). Bezeichner englisch,
|
||||||
|
// Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import type { LayerCategory, Project } from "../model/types";
|
||||||
|
import { flattenCategories } from "../model/types";
|
||||||
|
import { findCategory } from "../state/projectSlice";
|
||||||
|
import { MenuIcon } from "../ui/ContextMenu";
|
||||||
|
import type { ContextMenuItem } from "../ui/ContextMenu";
|
||||||
|
|
||||||
|
/** Offenes Kontextmenü (genau eines). `x/y` = Anker in Viewport-Px. */
|
||||||
|
export type MenuState =
|
||||||
|
| { kind: "layer"; code: string; x: number; y: number }
|
||||||
|
| { kind: "level"; id: string; x: number; y: number }
|
||||||
|
| { kind: "plan"; wallId: string | null; x: number; y: number }
|
||||||
|
| { kind: "viewport"; wallId: string | null; x: number; y: number }
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/** Titel des Kontextmenüs je Quelle (Ebenenname / Geschossname / Wand/Ansicht). */
|
||||||
|
export function menuTitle(menu: NonNullable<MenuState>, project: Project): string {
|
||||||
|
switch (menu.kind) {
|
||||||
|
case "layer": {
|
||||||
|
const c = findCategory(project.layers, menu.code);
|
||||||
|
return c ? `${c.code} · ${c.name}` : menu.code;
|
||||||
|
}
|
||||||
|
case "level": {
|
||||||
|
const l = project.drawingLevels.find((z) => z.id === menu.id);
|
||||||
|
return l ? l.name : t("ctx.title.level");
|
||||||
|
}
|
||||||
|
case "plan":
|
||||||
|
case "viewport":
|
||||||
|
return menu.wallId ? t("ctx.title.wall") : t("ctx.title.view");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aktionen, die der Kontextmenü-Builder von App braucht (alle verdrahtet). */
|
||||||
|
export interface MenuActions {
|
||||||
|
project: Project;
|
||||||
|
selectedWallId: string | null;
|
||||||
|
layerClipboard: { color: string; lw: number } | null;
|
||||||
|
openLayerEditor: (code: string) => void;
|
||||||
|
openLevelEditor: (id: string) => void;
|
||||||
|
addSubCategory: (parentCode: string) => void;
|
||||||
|
assignWallCategory: (wallId: string, code: string) => void;
|
||||||
|
duplicateCategory: (code: string) => void;
|
||||||
|
deleteCategory: (code: string) => void;
|
||||||
|
setLayerClipboard: (clip: { color: string; lw: number } | null) => void;
|
||||||
|
patchCategory: (code: string, patch: Partial<LayerCategory>) => void;
|
||||||
|
duplicateLevel: (id: string) => void;
|
||||||
|
deleteLevel: (id: string) => void;
|
||||||
|
clearSelection: () => void;
|
||||||
|
fit: () => void;
|
||||||
|
fitSelection: () => void;
|
||||||
|
/** Anzahl geschlossener Flächen in der aktuellen 2D-Auswahl (Boolean-Aktivierung). */
|
||||||
|
closedSelectionCount: number;
|
||||||
|
union: () => void;
|
||||||
|
difference: () => void;
|
||||||
|
intersect: () => void;
|
||||||
|
/** Anzahl aktuell selektierter 2D-Zeichnungselemente (z-Anordnen-Menü). */
|
||||||
|
drawingSelectionCount: number;
|
||||||
|
bringToFront: () => void;
|
||||||
|
bringForward: () => void;
|
||||||
|
sendBackward: () => void;
|
||||||
|
sendToBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die Kontextmenü-Einträge je Quelle — 1:1 nach docs/design/context-menu.md.
|
||||||
|
* JEDER Eintrag ist verdrahtet ODER klar deaktiviert mit Tooltip (`title`,
|
||||||
|
* nennt den Grund) — kein stiller No-Op.
|
||||||
|
*/
|
||||||
|
export function buildMenuItems(
|
||||||
|
menu: NonNullable<MenuState>,
|
||||||
|
a: MenuActions,
|
||||||
|
): ContextMenuItem[] {
|
||||||
|
switch (menu.kind) {
|
||||||
|
case "layer":
|
||||||
|
return layerMenuItems(menu.code, a);
|
||||||
|
case "level":
|
||||||
|
return levelMenuItems(menu.id, a);
|
||||||
|
case "plan":
|
||||||
|
return planMenuItems(menu.wallId, a);
|
||||||
|
case "viewport":
|
||||||
|
return viewportMenuItems(menu.wallId, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ebenen-Kontextmenü (Rechtsklick auf Ebenen-Zeile). */
|
||||||
|
function layerMenuItems(code: string, a: MenuActions): ContextMenuItem[] {
|
||||||
|
const total = flattenCategories(a.project.layers).length;
|
||||||
|
const hasSelection = a.selectedWallId !== null;
|
||||||
|
const hasClipboard = a.layerClipboard !== null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t("ctx.layerSettings"),
|
||||||
|
icon: <MenuIcon name="settings" />,
|
||||||
|
onClick: () => a.openLayerEditor(code),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.addSubLayer"),
|
||||||
|
icon: <MenuIcon name="add" />,
|
||||||
|
onClick: () => a.addSubCategory(code),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.assignSelection"),
|
||||||
|
icon: <MenuIcon name="move_down" />,
|
||||||
|
disabled: !hasSelection,
|
||||||
|
title: hasSelection ? undefined : t("ctx.assignSelection.disabled"),
|
||||||
|
onClick: () => {
|
||||||
|
if (a.selectedWallId) a.assignWallCategory(a.selectedWallId, code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.duplicate"),
|
||||||
|
icon: <MenuIcon name="content_copy" />,
|
||||||
|
onClick: () => a.duplicateCategory(code),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.copyProps"),
|
||||||
|
icon: <MenuIcon name="colorize" />,
|
||||||
|
onClick: () => {
|
||||||
|
const c = findCategory(a.project.layers, code);
|
||||||
|
if (c) a.setLayerClipboard({ color: c.color, lw: c.lw });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.pasteProps"),
|
||||||
|
icon: <MenuIcon name="format_paint" />,
|
||||||
|
disabled: !hasClipboard,
|
||||||
|
title: hasClipboard ? undefined : t("ctx.pasteProps.disabled"),
|
||||||
|
onClick: () => {
|
||||||
|
if (a.layerClipboard)
|
||||||
|
a.patchCategory(code, {
|
||||||
|
color: a.layerClipboard.color,
|
||||||
|
lw: a.layerClipboard.lw,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.delete"),
|
||||||
|
icon: <MenuIcon name="delete" />,
|
||||||
|
danger: true,
|
||||||
|
disabled: total <= 1,
|
||||||
|
title: total <= 1 ? t("ctx.delete.lastLayer") : undefined,
|
||||||
|
onClick: () => a.deleteCategory(code),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeichnungsebenen-Kontextmenü (Rechtsklick auf Geschoss/Schnitt/Zeichnung). */
|
||||||
|
function levelMenuItems(id: string, a: MenuActions): ContextMenuItem[] {
|
||||||
|
const total = a.project.drawingLevels.length;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t("ctx.levelSettings"),
|
||||||
|
icon: <MenuIcon name="settings" />,
|
||||||
|
onClick: () => a.openLevelEditor(id),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.duplicate"),
|
||||||
|
icon: <MenuIcon name="content_copy" />,
|
||||||
|
onClick: () => a.duplicateLevel(id),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.delete"),
|
||||||
|
icon: <MenuIcon name="delete" />,
|
||||||
|
danger: true,
|
||||||
|
disabled: total <= 1,
|
||||||
|
title: total <= 1 ? t("ctx.delete.lastLevel") : undefined,
|
||||||
|
onClick: () => a.deleteLevel(id),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grundriss-Kontextmenü. Bezug ist die getroffene/gewählte Wand. Einträge sind
|
||||||
|
* verdrahtet: „Auf Auswahl einpassen" (PlanView-Handle) und „Auswahl aufheben".
|
||||||
|
* Ohne getroffene Wand bleibt nur „Einpassen". (Eigenschaften-Panel/Bemaßung
|
||||||
|
* folgen später — daher hier KEIN Schein-Eintrag.)
|
||||||
|
*/
|
||||||
|
function planMenuItems(
|
||||||
|
wallId: string | null,
|
||||||
|
a: MenuActions,
|
||||||
|
): ContextMenuItem[] {
|
||||||
|
// Boolean-Einträge (Vereinigen/Subtrahieren/Schneiden) auf die 2D-Auswahl.
|
||||||
|
// Aktiv ab 2 geschlossenen Flächen; sonst deaktiviert mit erklärendem Tooltip.
|
||||||
|
const boolEnabled = a.closedSelectionCount >= 2;
|
||||||
|
const boolTitle = boolEnabled ? undefined : t("ctx.boolean.disabled");
|
||||||
|
const booleanItems: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: t("ctx.union"),
|
||||||
|
icon: <MenuIcon name="join_full" />,
|
||||||
|
disabled: !boolEnabled,
|
||||||
|
title: boolTitle,
|
||||||
|
onClick: a.union,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.subtract"),
|
||||||
|
icon: <MenuIcon name="join_left" />,
|
||||||
|
disabled: !boolEnabled,
|
||||||
|
title: boolTitle,
|
||||||
|
onClick: a.difference,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.intersect"),
|
||||||
|
icon: <MenuIcon name="join_inner" />,
|
||||||
|
disabled: !boolEnabled,
|
||||||
|
title: boolTitle,
|
||||||
|
onClick: a.intersect,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Z-Anordnen (nach vorn/hinten holen) — wirkt auf die aktuelle 2D-Auswahl,
|
||||||
|
// sortiert `drawings2d` um (Array-Reihenfolge = Zeichenreihenfolge). Aktiv
|
||||||
|
// ab 1 selektiertem Element; sonst deaktiviert mit erklärendem Tooltip.
|
||||||
|
const arrangeEnabled = a.drawingSelectionCount >= 1;
|
||||||
|
const arrangeTitle = arrangeEnabled ? undefined : t("ctx.arrange.disabled");
|
||||||
|
const arrangeItems: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.front"),
|
||||||
|
icon: <MenuIcon name="flip_to_front" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.bringToFront,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.forward"),
|
||||||
|
icon: <MenuIcon name="arrow_upward" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.bringForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.backward"),
|
||||||
|
icon: <MenuIcon name="arrow_downward" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.sendBackward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.back"),
|
||||||
|
icon: <MenuIcon name="flip_to_back" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.sendToBack,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (wallId) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t("ctx.fitSelection"),
|
||||||
|
icon: <MenuIcon name="center_focus_strong" />,
|
||||||
|
onClick: a.fitSelection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.clearSelection"),
|
||||||
|
icon: <MenuIcon name="deselect" />,
|
||||||
|
onClick: a.clearSelection,
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
...booleanItems,
|
||||||
|
{ divider: true },
|
||||||
|
...arrangeItems,
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.fit"),
|
||||||
|
icon: <MenuIcon name="fit_screen" />,
|
||||||
|
onClick: a.fit,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...booleanItems,
|
||||||
|
{ divider: true },
|
||||||
|
...arrangeItems,
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
label: t("ctx.fit"),
|
||||||
|
icon: <MenuIcon name="fit_screen" />,
|
||||||
|
onClick: a.fit,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D-Kontextmenü. Im 3D gibt es (noch) kein Einpassen-Handle wie im Grundriss;
|
||||||
|
* verdrahtet ist „Auswahl aufheben" (mit getroffener Wand). Ohne Wand ist das
|
||||||
|
* Menü leer-sinnvoll: ein deaktivierter Hinweis, kein stiller No-Op.
|
||||||
|
*/
|
||||||
|
function viewportMenuItems(
|
||||||
|
wallId: string | null,
|
||||||
|
a: MenuActions,
|
||||||
|
): ContextMenuItem[] {
|
||||||
|
if (wallId) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t("ctx.clearSelection"),
|
||||||
|
icon: <MenuIcon name="deselect" />,
|
||||||
|
onClick: a.clearSelection,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t("ctx.noWallHit"),
|
||||||
|
disabled: true,
|
||||||
|
title: t("ctx.noWallHit.hint"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
+8
-1
@@ -2080,9 +2080,16 @@ body {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auswahl-Hervorhebung (Links-Klick): Akzentkontur + leichte Füllung. */
|
/* Auswahl-Hervorhebung (Links-Klick): Akzentkontur + leichte Füllung.
|
||||||
|
`fill-opacity` erzwingt die Transparenz unabhängig vom Theme — im Hell-Theme
|
||||||
|
ist `--accent-dim` fast deckend weiss (Nutzung als UI-Hintergrundfarbe an
|
||||||
|
~70 anderen Stellen), würde ohne eigene Opacity die Schraffur der gewählten
|
||||||
|
Wand/Decke/des Raums vollständig verdecken (nur im Dunkel-Theme zufällig
|
||||||
|
unauffällig, da dort selbst schon rgba mit niedriger Alpha). Gleiches Muster
|
||||||
|
wie .plan-marquee/.tool-preview-fill weiter unten. */
|
||||||
.plan-svg .plan-selected {
|
.plan-svg .plan-selected {
|
||||||
fill: var(--accent-dim);
|
fill: var(--accent-dim);
|
||||||
|
fill-opacity: 0.35;
|
||||||
stroke: var(--accent);
|
stroke: var(--accent);
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Kleiner schwebender Editor (dunkel, wie das Kontextmenü) am Anker. Schließt
|
||||||
|
// über Außenklick / Esc. Rein darstellend: die Felder rufen die übergebenen
|
||||||
|
// Patch-Handler, die live auf den State wirken (kein Bestätigen nötig).
|
||||||
|
//
|
||||||
|
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
|
export function InlineEditor({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
title: string;
|
||||||
|
onClose: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// An die Viewport-Ränder klemmen (wie das Kontextmenü).
|
||||||
|
const [pos, setPos] = useState({ x, y });
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
let nx = x;
|
||||||
|
let ny = y;
|
||||||
|
if (nx + r.width > window.innerWidth - 4) nx = window.innerWidth - r.width - 4;
|
||||||
|
if (ny + r.height > window.innerHeight - 4)
|
||||||
|
ny = window.innerHeight - r.height - 4;
|
||||||
|
setPos({ x: Math.max(4, nx), y: Math.max(4, ny) });
|
||||||
|
}, [x, y]);
|
||||||
|
// Schließen bei Außenklick / Esc.
|
||||||
|
useEffect(() => {
|
||||||
|
const onDown = (e: PointerEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||||
|
};
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener("pointerdown", onDown, true);
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerdown", onDown, true);
|
||||||
|
window.removeEventListener("keydown", onKey);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="ctx-editor"
|
||||||
|
style={{ left: pos.x, top: pos.y }}
|
||||||
|
role="dialog"
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
<div className="ctx-editor-head">
|
||||||
|
<span className="ctx-editor-title">{title}</span>
|
||||||
|
<button
|
||||||
|
className="ctx-editor-close"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label={t("editor.close")}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ctx-editor-body">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,855 @@
|
|||||||
|
// Haupt-Viewport-Inhalt (Mitte) — abhängig von aktiver Zeichnungsebene und
|
||||||
|
// Ansichtstyp. `Content` ist der View-Router: Geschoss→Grundriss/3D,
|
||||||
|
// Schnitt/Ansicht→SectionPlanView, freie Zeichnung→LevelPlanView. Die beiden
|
||||||
|
// Plan-Ableitungen (Grundriss bzw. Schnitt/Ansicht) zeichnen dieselbe PlanView.
|
||||||
|
//
|
||||||
|
// Extrahiert aus App.tsx (top-level, alles über Props — keine App-Closure).
|
||||||
|
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { formatM } from "../model/types";
|
||||||
|
import type {
|
||||||
|
Ceiling,
|
||||||
|
Drawing2D,
|
||||||
|
DrawingLevel,
|
||||||
|
EdgeGrip,
|
||||||
|
Project,
|
||||||
|
Roof,
|
||||||
|
Vec2,
|
||||||
|
Wall,
|
||||||
|
} from "../model/types";
|
||||||
|
import { generatePlan, generateSectionPlan } from "../plan/generatePlan";
|
||||||
|
import type { CategoryDisplayResolver } from "../plan/generatePlan";
|
||||||
|
import { computeSection } from "../plan/toSection";
|
||||||
|
import type { SectionOutput } from "../plan/toSection";
|
||||||
|
import { generateElevationPlan } from "../plan/toElevation";
|
||||||
|
import { pushNativeScene, pushNativeWalls } from "../plan/nativeSync";
|
||||||
|
import { selectionHighlightLines } from "../plan/toWalls3d";
|
||||||
|
import { PlanView } from "../plan/PlanView";
|
||||||
|
import type {
|
||||||
|
GripHandlers,
|
||||||
|
HudFieldsState,
|
||||||
|
MarqueeSelection,
|
||||||
|
PlanSelection,
|
||||||
|
PlanViewHandle,
|
||||||
|
} from "../plan/PlanView";
|
||||||
|
import { Viewport3D } from "../viewport/Viewport3D";
|
||||||
|
import type { DisplayResolver, ViewportContextInfo } from "../viewport/Viewport3D";
|
||||||
|
import type { Pick3dHit } from "../viewport/Wasm3DViewport";
|
||||||
|
import { DrawingTabs } from "../DrawingTabs";
|
||||||
|
import type { Drawing } from "../DrawingTabs";
|
||||||
|
import type { ToolDraft, ToolHandlers, ToolId } from "../tools/types";
|
||||||
|
import type { DetailLevel, RenderMode, View3d, ViewType } from "../ui/TopBar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Akzentfarbe der 3D-Auswahl-Hervorhebung (Umriss-Linien, WASM-Viewport, s.
|
||||||
|
* `Content`/`selectionHighlightLines`). Kräftiges Orange — hebt sich vom
|
||||||
|
* grauen Wand-/Deckenton, dem hellen Hidden-Line-Weiss und dem Bodenraster ab.
|
||||||
|
*/
|
||||||
|
const HIGHLIGHT_RGB: [number, number, number] = [1.0, 0.5, 0.1];
|
||||||
|
|
||||||
|
/** Hauptinhalt — abhängig von der aktiven Zeichnungsebene und dem Ansichtstyp. */
|
||||||
|
export function Content({
|
||||||
|
project,
|
||||||
|
level,
|
||||||
|
viewType,
|
||||||
|
visibleCodes,
|
||||||
|
detail,
|
||||||
|
referenceLines,
|
||||||
|
hairline,
|
||||||
|
marqueeColor,
|
||||||
|
snapColor,
|
||||||
|
renderMode,
|
||||||
|
view3d,
|
||||||
|
fov,
|
||||||
|
grid3dVisible,
|
||||||
|
onToggleGrid3d,
|
||||||
|
section3d,
|
||||||
|
onClearSection3d,
|
||||||
|
mono,
|
||||||
|
drawings,
|
||||||
|
onSelectLevel,
|
||||||
|
floorDisplay,
|
||||||
|
categoryDisplay,
|
||||||
|
displayKey,
|
||||||
|
planViewRef,
|
||||||
|
onCursor,
|
||||||
|
onScale,
|
||||||
|
selectedWallIds,
|
||||||
|
selectedCeilingIds,
|
||||||
|
selectedOpeningIds,
|
||||||
|
selectedStairIds,
|
||||||
|
selectedExtrudedSolidIds,
|
||||||
|
selectedColumnIds,
|
||||||
|
selectedRoomIds,
|
||||||
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
|
selectedSectionLineId,
|
||||||
|
roomStamp,
|
||||||
|
onStampMove,
|
||||||
|
onStampEdit,
|
||||||
|
onEditDrawingText,
|
||||||
|
onPlanSelect,
|
||||||
|
onPlanMarquee,
|
||||||
|
onSegmentCut,
|
||||||
|
onPlanContextMenu,
|
||||||
|
onViewportSelectWall,
|
||||||
|
onViewportSelectCeiling,
|
||||||
|
onViewportSelectOpening,
|
||||||
|
onViewportSelectStair,
|
||||||
|
onViewportContextMenu,
|
||||||
|
onViewport3dPick,
|
||||||
|
commandActive,
|
||||||
|
draft,
|
||||||
|
onWorkplanePoint,
|
||||||
|
onWorkplaneConfirm,
|
||||||
|
activeTool,
|
||||||
|
toolInputActive,
|
||||||
|
toolHandlers,
|
||||||
|
hudFields,
|
||||||
|
grips,
|
||||||
|
edgeGrips,
|
||||||
|
selectedDrawingId,
|
||||||
|
selectedDrawingIds,
|
||||||
|
gripHandlers,
|
||||||
|
edit3dWalls,
|
||||||
|
edit3dDrawings,
|
||||||
|
edit3dCeilings,
|
||||||
|
edit3dRoofs,
|
||||||
|
onEdit3dVertex,
|
||||||
|
onEdit3dBody,
|
||||||
|
onEdit3dEdge,
|
||||||
|
onEdit3dRoofPitch,
|
||||||
|
onEdit3dWallTop,
|
||||||
|
onEdit3dEnd,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
level: DrawingLevel;
|
||||||
|
viewType: ViewType;
|
||||||
|
visibleCodes: Set<string>;
|
||||||
|
detail: DetailLevel;
|
||||||
|
referenceLines: boolean;
|
||||||
|
hairline: boolean;
|
||||||
|
/** Farbe des Auswahlrahmens (Marquee) im Grundriss. */
|
||||||
|
marqueeColor: string;
|
||||||
|
/** Farbe der Snap-/Endpunkt-Hervorhebung im Grundriss. */
|
||||||
|
snapColor: string;
|
||||||
|
renderMode: RenderMode;
|
||||||
|
/** Kanonischer 3D-Blickwinkel (Preset) der Perspektive. */
|
||||||
|
view3d: View3d;
|
||||||
|
/** Sichtwinkel der 3D-Perspektivkamera in Grad. */
|
||||||
|
fov: number;
|
||||||
|
/** Boden-Referenzraster im 3D-Viewport sichtbar (wirkt nur im WASM-Pfad). */
|
||||||
|
grid3dVisible: boolean;
|
||||||
|
/** Klick auf den Grid-Umschalter-Button im 3D-Viewport (WASM-Pfad). */
|
||||||
|
onToggleGrid3d: () => void;
|
||||||
|
/** Aktive 3D-Schnittebene (world point+normal) der gebundenen Schnittlinie,
|
||||||
|
* oder null. Speist Viewport3D/Wasm3DViewport (Live-Schnitt). */
|
||||||
|
section3d: { point: [number, number, number]; normal: [number, number, number] } | null;
|
||||||
|
/** Schnitt-Bindung im 3D-Viewport lösen (Klick auf den gebundenen Knopf). */
|
||||||
|
onClearSection3d: () => void;
|
||||||
|
/** Grundriss in Schwarz-Weiss (mono) statt mit Kategorie-/Element-Farben. */
|
||||||
|
mono: boolean;
|
||||||
|
/** Dokument-Tabs der Kopfzeile — ein Tab je Geschoss-Grundriss. */
|
||||||
|
drawings: Drawing[];
|
||||||
|
/** Aktiviert eine Zeichnungsebene (Geschoss) — geteilt mit dem Panel
|
||||||
|
* „Zeichnungsebenen"; siehe DrawingTabs.tsx. */
|
||||||
|
onSelectLevel: (id: string) => void;
|
||||||
|
floorDisplay: DisplayResolver;
|
||||||
|
categoryDisplay: CategoryDisplayResolver & DisplayResolver;
|
||||||
|
displayKey: string;
|
||||||
|
planViewRef: React.RefObject<PlanViewHandle>;
|
||||||
|
onCursor: (model: Vec2 | null) => void;
|
||||||
|
onScale: (n: number) => void;
|
||||||
|
selectedWallIds: string[];
|
||||||
|
selectedCeilingIds: string[];
|
||||||
|
selectedOpeningIds: string[];
|
||||||
|
selectedStairIds: string[];
|
||||||
|
selectedExtrudedSolidIds: string[];
|
||||||
|
selectedColumnIds: string[];
|
||||||
|
selectedRoomIds: string[];
|
||||||
|
selectedRoofIds: string[];
|
||||||
|
selectedContextObjectIds: string[];
|
||||||
|
selectedSectionLineId: string | null;
|
||||||
|
roomStamp: { roomId: string; anchor: Vec2 } | null;
|
||||||
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
|
onStampEdit: (roomId: string) => void;
|
||||||
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
|
onSegmentCut: (drawingId: string, model: Vec2) => void;
|
||||||
|
onPlanContextMenu: (info: {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
model: Vec2;
|
||||||
|
}) => void;
|
||||||
|
onViewportSelectWall: (wallId: string | null) => void;
|
||||||
|
onViewportSelectCeiling: (ceilingId: string | null) => void;
|
||||||
|
onViewportSelectOpening: (openingId: string | null) => void;
|
||||||
|
onViewportSelectStair: (stairId: string | null) => void;
|
||||||
|
onViewportContextMenu: (info: ViewportContextInfo) => void;
|
||||||
|
/** Links-KLICK-Auswahl im WASM-3D-Viewport (TS-Raycast, Wand/Decke). */
|
||||||
|
onViewport3dPick: (hit: Pick3dHit, additive: boolean) => void;
|
||||||
|
/** Läuft ein zeichnender Befehl? Macht den 3D-Viewport zur Erstellungs-Fläche. */
|
||||||
|
commandActive: boolean;
|
||||||
|
/** Aktuelle Werkzeug-Vorschau (Rubber-Band) — auch in 3D gespiegelt. */
|
||||||
|
draft: ToolDraft | null;
|
||||||
|
/** Auf die Arbeitsebene projizierter Punkt → CommandEngine (move/pick). */
|
||||||
|
onWorkplanePoint: (kind: "move" | "pick", model: Vec2) => void;
|
||||||
|
/** Befehl bestätigen/beenden aus dem 3D-Viewport. */
|
||||||
|
onWorkplaneConfirm: () => void;
|
||||||
|
activeTool: ToolId;
|
||||||
|
toolInputActive: boolean;
|
||||||
|
toolHandlers: ToolHandlers;
|
||||||
|
hudFields: HudFieldsState | null;
|
||||||
|
grips: Vec2[];
|
||||||
|
edgeGrips: EdgeGrip[];
|
||||||
|
selectedDrawingId: string | null;
|
||||||
|
selectedDrawingIds?: string[];
|
||||||
|
gripHandlers: GripHandlers;
|
||||||
|
/** Alle gewählten Wände — Quelle der 3D-Editier-Griffe (Mehrfachauswahl ⇒ alle). */
|
||||||
|
edit3dWalls: Wall[];
|
||||||
|
/** Alle gewählten 2D-Elemente — 3D-Vertex-Griffe (Mehrfachauswahl ⇒ alle). */
|
||||||
|
edit3dDrawings: Drawing2D[];
|
||||||
|
/** Alle gewählten Decken — 3D-Eckpunkt-/Kanten-/Verschiebe-Griffe. */
|
||||||
|
edit3dCeilings: Ceiling[];
|
||||||
|
edit3dRoofs: Roof[];
|
||||||
|
onEdit3dVertex: (
|
||||||
|
target: { wallId?: string; drawingId?: string; ceilingId?: string; roofId?: string },
|
||||||
|
index: number,
|
||||||
|
model: Vec2,
|
||||||
|
excludeWallIds?: Set<string>,
|
||||||
|
excludeDrawingIds?: Set<string>,
|
||||||
|
) => Vec2;
|
||||||
|
onEdit3dBody: (
|
||||||
|
target: { wallId?: string; drawingId?: string; ceilingId?: string; roofId?: string },
|
||||||
|
delta: Vec2,
|
||||||
|
) => void;
|
||||||
|
onEdit3dEdge: (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => void;
|
||||||
|
onEdit3dRoofPitch: (roofId: string, apexHeight: number) => void;
|
||||||
|
onEdit3dWallTop: (wallId: string, topZ: number) => void;
|
||||||
|
onEdit3dEnd: () => void;
|
||||||
|
}) {
|
||||||
|
// Auswahl-Hervorhebung im WASM-3D-Viewport: Umriss der gewählten Wand(e)/
|
||||||
|
// Decke(n) in Akzent-Orange, immer obenauf (s. selectionHighlightLines in
|
||||||
|
// plan/toWalls3d.ts + set_highlight_lines in useWasm3dRenderer). Wirkt NUR
|
||||||
|
// im WASM-Zweig (Viewport3D reicht sie nur dort an Wasm3DViewport durch);
|
||||||
|
// die three.js-Sicht hebt die Auswahl weiterhin über ihr eigenes Material
|
||||||
|
// hervor. Leere Auswahl → null (kein Highlight).
|
||||||
|
const highlightLines = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedWallIds.length === 0 &&
|
||||||
|
selectedCeilingIds.length === 0 &&
|
||||||
|
selectedOpeningIds.length === 0 &&
|
||||||
|
selectedStairIds.length === 0 &&
|
||||||
|
selectedRoofIds.length === 0 &&
|
||||||
|
selectedContextObjectIds.length === 0
|
||||||
|
? null
|
||||||
|
: selectionHighlightLines(
|
||||||
|
project,
|
||||||
|
selectedWallIds,
|
||||||
|
selectedCeilingIds,
|
||||||
|
HIGHLIGHT_RGB,
|
||||||
|
selectedOpeningIds,
|
||||||
|
selectedStairIds,
|
||||||
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
project,
|
||||||
|
selectedWallIds,
|
||||||
|
selectedCeilingIds,
|
||||||
|
selectedOpeningIds,
|
||||||
|
selectedStairIds,
|
||||||
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (level.kind === "section" || level.kind === "elevation") {
|
||||||
|
return (
|
||||||
|
<main className="content">
|
||||||
|
<DrawingTabs
|
||||||
|
drawings={drawings}
|
||||||
|
activeLevelId={level.id}
|
||||||
|
onSelectLevel={onSelectLevel}
|
||||||
|
/>
|
||||||
|
<SectionPlanView
|
||||||
|
project={project}
|
||||||
|
level={level}
|
||||||
|
hairline={hairline}
|
||||||
|
marqueeColor={marqueeColor}
|
||||||
|
snapColor={snapColor}
|
||||||
|
mono={mono}
|
||||||
|
planViewRef={planViewRef}
|
||||||
|
onCursor={onCursor}
|
||||||
|
onScale={onScale}
|
||||||
|
selectedWallIds={selectedWallIds}
|
||||||
|
selectedCeilingIds={selectedCeilingIds}
|
||||||
|
selectedOpeningIds={selectedOpeningIds}
|
||||||
|
selectedStairIds={selectedStairIds}
|
||||||
|
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
selectedRoomIds={selectedRoomIds}
|
||||||
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedSectionLineId={selectedSectionLineId}
|
||||||
|
roomStamp={roomStamp}
|
||||||
|
onStampMove={onStampMove}
|
||||||
|
onStampEdit={onStampEdit}
|
||||||
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
onOpenSectionLevel={onSelectLevel}
|
||||||
|
onPlanSelect={onPlanSelect}
|
||||||
|
onPlanMarquee={onPlanMarquee}
|
||||||
|
onSegmentCut={onSegmentCut}
|
||||||
|
onPlanContextMenu={onPlanContextMenu}
|
||||||
|
activeTool={activeTool}
|
||||||
|
toolInputActive={toolInputActive}
|
||||||
|
toolHandlers={toolHandlers}
|
||||||
|
hudFields={hudFields}
|
||||||
|
grips={grips}
|
||||||
|
edgeGrips={edgeGrips}
|
||||||
|
selectedDrawingId={selectedDrawingId}
|
||||||
|
selectedDrawingIds={selectedDrawingIds}
|
||||||
|
gripHandlers={gripHandlers}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freie Zeichnung: dieselbe Plan-Ansicht wie ein Geschoss (generatePlan filtert
|
||||||
|
// `drawings2d` nach `level.id`), nur OHNE Geschoss-Eigenschaftsleiste und ohne
|
||||||
|
// 3D-Umschaltung — eine Zeichnung hat keine Höhe/3D. So sind importierte
|
||||||
|
// DXF-Polylinien hier sofort sichtbar und mit den 2D-Werkzeugen bearbeitbar.
|
||||||
|
if (level.kind === "drawing") {
|
||||||
|
return (
|
||||||
|
<main className="content">
|
||||||
|
<DrawingTabs
|
||||||
|
drawings={drawings}
|
||||||
|
activeLevelId={level.id}
|
||||||
|
onSelectLevel={onSelectLevel}
|
||||||
|
/>
|
||||||
|
<LevelPlanView
|
||||||
|
project={project}
|
||||||
|
level={level}
|
||||||
|
visibleCodes={visibleCodes}
|
||||||
|
categoryDisplay={categoryDisplay}
|
||||||
|
detail={detail}
|
||||||
|
referenceLines={referenceLines}
|
||||||
|
hairline={hairline}
|
||||||
|
marqueeColor={marqueeColor}
|
||||||
|
snapColor={snapColor}
|
||||||
|
mono={mono}
|
||||||
|
planViewRef={planViewRef}
|
||||||
|
onCursor={onCursor}
|
||||||
|
onScale={onScale}
|
||||||
|
selectedWallIds={selectedWallIds}
|
||||||
|
selectedCeilingIds={selectedCeilingIds}
|
||||||
|
selectedOpeningIds={selectedOpeningIds}
|
||||||
|
selectedStairIds={selectedStairIds}
|
||||||
|
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
selectedRoomIds={selectedRoomIds}
|
||||||
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedContextObjectIds={selectedContextObjectIds}
|
||||||
|
selectedSectionLineId={selectedSectionLineId}
|
||||||
|
roomStamp={roomStamp}
|
||||||
|
onStampMove={onStampMove}
|
||||||
|
onStampEdit={onStampEdit}
|
||||||
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
onOpenSectionLevel={onSelectLevel}
|
||||||
|
onPlanSelect={onPlanSelect}
|
||||||
|
onPlanMarquee={onPlanMarquee}
|
||||||
|
onSegmentCut={onSegmentCut}
|
||||||
|
onPlanContextMenu={onPlanContextMenu}
|
||||||
|
activeTool={activeTool}
|
||||||
|
toolInputActive={toolInputActive}
|
||||||
|
toolHandlers={toolHandlers}
|
||||||
|
hudFields={hudFields}
|
||||||
|
grips={grips}
|
||||||
|
edgeGrips={edgeGrips}
|
||||||
|
selectedDrawingId={selectedDrawingId}
|
||||||
|
selectedDrawingIds={selectedDrawingIds}
|
||||||
|
gripHandlers={gripHandlers}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geschoss: Dokument-Tabs + gewählte Ansicht. Geschosshöhe/Schnitthöhe
|
||||||
|
// werden in den Geschoss-Einstellungen gepflegt (Rechtsklick auf das
|
||||||
|
// Geschoss im Panel „Zeichnungsebenen"), nicht mehr hier in der Kopfzeile.
|
||||||
|
return (
|
||||||
|
<main className="content">
|
||||||
|
<DrawingTabs
|
||||||
|
drawings={drawings}
|
||||||
|
activeLevelId={level.id}
|
||||||
|
onSelectLevel={onSelectLevel}
|
||||||
|
/>
|
||||||
|
{viewType === "perspektive" ? (
|
||||||
|
<section className="pane">
|
||||||
|
<div className="pane-title">
|
||||||
|
{t("content.perspektiveTitle", { name: level.name })}
|
||||||
|
</div>
|
||||||
|
<Viewport3D
|
||||||
|
project={project}
|
||||||
|
visibleCodes={visibleCodes}
|
||||||
|
floorDisplay={floorDisplay}
|
||||||
|
categoryDisplay={categoryDisplay}
|
||||||
|
displayKey={displayKey}
|
||||||
|
renderMode={renderMode}
|
||||||
|
detail={detail}
|
||||||
|
view3d={view3d}
|
||||||
|
fov={fov}
|
||||||
|
gridElevation={level.baseElevation ?? 0}
|
||||||
|
gridVisible={grid3dVisible}
|
||||||
|
onToggleGrid={onToggleGrid3d}
|
||||||
|
section3d={section3d}
|
||||||
|
onClearSection3d={onClearSection3d}
|
||||||
|
onSelectWall={onViewportSelectWall}
|
||||||
|
onSelectCeiling={onViewportSelectCeiling}
|
||||||
|
onSelectOpening={onViewportSelectOpening}
|
||||||
|
onSelectStair={onViewportSelectStair}
|
||||||
|
onContextMenu={onViewportContextMenu}
|
||||||
|
onPick3d={onViewport3dPick}
|
||||||
|
highlightLines={highlightLines}
|
||||||
|
commandActive={commandActive}
|
||||||
|
draft={draft}
|
||||||
|
onWorkplanePoint={onWorkplanePoint}
|
||||||
|
onWorkplaneConfirm={onWorkplaneConfirm}
|
||||||
|
editWalls={edit3dWalls}
|
||||||
|
editDrawings={edit3dDrawings}
|
||||||
|
editCeilings={edit3dCeilings}
|
||||||
|
editRoofs={edit3dRoofs}
|
||||||
|
onEditVertex={onEdit3dVertex}
|
||||||
|
onEditBody={onEdit3dBody}
|
||||||
|
onEditEdge={onEdit3dEdge}
|
||||||
|
onEditRoofPitch={onEdit3dRoofPitch}
|
||||||
|
onEditWallTop={onEdit3dWallTop}
|
||||||
|
onEditEnd={onEdit3dEnd}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<LevelPlanView
|
||||||
|
project={project}
|
||||||
|
level={level}
|
||||||
|
visibleCodes={visibleCodes}
|
||||||
|
categoryDisplay={categoryDisplay}
|
||||||
|
detail={detail}
|
||||||
|
referenceLines={referenceLines}
|
||||||
|
hairline={hairline}
|
||||||
|
marqueeColor={marqueeColor}
|
||||||
|
snapColor={snapColor}
|
||||||
|
mono={mono}
|
||||||
|
planViewRef={planViewRef}
|
||||||
|
onCursor={onCursor}
|
||||||
|
onScale={onScale}
|
||||||
|
selectedWallIds={selectedWallIds}
|
||||||
|
selectedCeilingIds={selectedCeilingIds}
|
||||||
|
selectedOpeningIds={selectedOpeningIds}
|
||||||
|
selectedStairIds={selectedStairIds}
|
||||||
|
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
selectedRoomIds={selectedRoomIds}
|
||||||
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedContextObjectIds={selectedContextObjectIds}
|
||||||
|
selectedSectionLineId={selectedSectionLineId}
|
||||||
|
roomStamp={roomStamp}
|
||||||
|
onStampMove={onStampMove}
|
||||||
|
onStampEdit={onStampEdit}
|
||||||
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
onOpenSectionLevel={onSelectLevel}
|
||||||
|
onPlanSelect={onPlanSelect}
|
||||||
|
onPlanMarquee={onPlanMarquee}
|
||||||
|
onSegmentCut={onSegmentCut}
|
||||||
|
onPlanContextMenu={onPlanContextMenu}
|
||||||
|
activeTool={activeTool}
|
||||||
|
toolInputActive={toolInputActive}
|
||||||
|
toolHandlers={toolHandlers}
|
||||||
|
hudFields={hudFields}
|
||||||
|
grips={grips}
|
||||||
|
edgeGrips={edgeGrips}
|
||||||
|
selectedDrawingId={selectedDrawingId}
|
||||||
|
selectedDrawingIds={selectedDrawingIds}
|
||||||
|
gripHandlers={gripHandlers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Abgeleiteter Grundriss eines Geschosses. */
|
||||||
|
function LevelPlanView({
|
||||||
|
project,
|
||||||
|
level,
|
||||||
|
visibleCodes,
|
||||||
|
categoryDisplay,
|
||||||
|
detail,
|
||||||
|
referenceLines,
|
||||||
|
hairline,
|
||||||
|
marqueeColor,
|
||||||
|
snapColor,
|
||||||
|
mono,
|
||||||
|
planViewRef,
|
||||||
|
onCursor,
|
||||||
|
onScale,
|
||||||
|
selectedWallIds,
|
||||||
|
selectedCeilingIds,
|
||||||
|
selectedOpeningIds,
|
||||||
|
selectedStairIds,
|
||||||
|
selectedExtrudedSolidIds,
|
||||||
|
selectedColumnIds,
|
||||||
|
selectedRoomIds,
|
||||||
|
selectedRoofIds,
|
||||||
|
selectedContextObjectIds,
|
||||||
|
selectedSectionLineId,
|
||||||
|
roomStamp,
|
||||||
|
onStampMove,
|
||||||
|
onStampEdit,
|
||||||
|
onEditDrawingText,
|
||||||
|
onOpenSectionLevel,
|
||||||
|
onPlanSelect,
|
||||||
|
onPlanMarquee,
|
||||||
|
onSegmentCut,
|
||||||
|
onPlanContextMenu,
|
||||||
|
activeTool,
|
||||||
|
toolInputActive,
|
||||||
|
toolHandlers,
|
||||||
|
hudFields,
|
||||||
|
grips,
|
||||||
|
edgeGrips,
|
||||||
|
selectedDrawingId,
|
||||||
|
selectedDrawingIds,
|
||||||
|
gripHandlers,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
level: DrawingLevel;
|
||||||
|
visibleCodes: Set<string>;
|
||||||
|
categoryDisplay: CategoryDisplayResolver;
|
||||||
|
detail: DetailLevel;
|
||||||
|
referenceLines: boolean;
|
||||||
|
hairline: boolean;
|
||||||
|
marqueeColor: string;
|
||||||
|
snapColor: string;
|
||||||
|
mono: boolean;
|
||||||
|
planViewRef: React.RefObject<PlanViewHandle>;
|
||||||
|
onCursor: (model: Vec2 | null) => void;
|
||||||
|
onScale: (n: number) => void;
|
||||||
|
selectedWallIds: string[];
|
||||||
|
selectedCeilingIds: string[];
|
||||||
|
selectedOpeningIds: string[];
|
||||||
|
selectedStairIds: string[];
|
||||||
|
selectedExtrudedSolidIds: string[];
|
||||||
|
selectedColumnIds: string[];
|
||||||
|
selectedRoomIds: string[];
|
||||||
|
selectedRoofIds: string[];
|
||||||
|
selectedContextObjectIds?: string[];
|
||||||
|
selectedSectionLineId: string | null;
|
||||||
|
roomStamp: { roomId: string; anchor: Vec2 } | null;
|
||||||
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
|
onStampEdit: (roomId: string) => void;
|
||||||
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
|
onSegmentCut: (drawingId: string, model: Vec2) => void;
|
||||||
|
onPlanContextMenu: (info: {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
model: Vec2;
|
||||||
|
}) => void;
|
||||||
|
activeTool: ToolId;
|
||||||
|
toolInputActive: boolean;
|
||||||
|
toolHandlers: ToolHandlers;
|
||||||
|
hudFields: HudFieldsState | null;
|
||||||
|
grips: Vec2[];
|
||||||
|
edgeGrips: EdgeGrip[];
|
||||||
|
selectedDrawingId: string | null;
|
||||||
|
selectedDrawingIds?: string[];
|
||||||
|
gripHandlers: GripHandlers;
|
||||||
|
}) {
|
||||||
|
// Detailgrad + Referenzlinien fließen in die Plan-Erzeugung ein (Symbole,
|
||||||
|
// Schraffuren/Linienfeinheit bzw. Wand-Achslinien).
|
||||||
|
const plan = useMemo(
|
||||||
|
() =>
|
||||||
|
generatePlan(
|
||||||
|
project,
|
||||||
|
level.id,
|
||||||
|
visibleCodes,
|
||||||
|
categoryDisplay,
|
||||||
|
detail,
|
||||||
|
referenceLines,
|
||||||
|
mono,
|
||||||
|
),
|
||||||
|
[project, level.id, visibleCodes, categoryDisplay, detail, referenceLines, mono],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Live-Spiegelung in die nativen wgpu-Fenster (No-op ohne Tauri, debounced):
|
||||||
|
// EXAKT der sichtbare Plan (gleiches Geschoss, gleicher Detailgrad) fürs
|
||||||
|
// 2D-Fenster, die Projekt-Wände fürs 3D-Fenster.
|
||||||
|
useEffect(() => {
|
||||||
|
pushNativeScene(plan);
|
||||||
|
}, [plan]);
|
||||||
|
useEffect(() => {
|
||||||
|
pushNativeWalls(project);
|
||||||
|
}, [project]);
|
||||||
|
|
||||||
|
// Bei einer freien Zeichnung gibt es keine Schnitthöhe → eigene Legende/Titel.
|
||||||
|
const isDrawing = level.kind === "drawing";
|
||||||
|
const caption = isDrawing
|
||||||
|
? t("content.drawingLegend")
|
||||||
|
: t("content.grundrissLegend", {
|
||||||
|
name: level.name,
|
||||||
|
cutHeight: formatM(level.cutHeight ?? 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="pane">
|
||||||
|
<div className="pane-title">
|
||||||
|
{isDrawing
|
||||||
|
? t("content.drawingTitle", { name: level.name })
|
||||||
|
: t("content.grundrissTitle", { name: level.name })}
|
||||||
|
</div>
|
||||||
|
<PlanView
|
||||||
|
ref={planViewRef}
|
||||||
|
plan={plan}
|
||||||
|
resetKey={level.id}
|
||||||
|
onCursor={onCursor}
|
||||||
|
onScale={onScale}
|
||||||
|
selectedWallIds={selectedWallIds}
|
||||||
|
selectedCeilingIds={selectedCeilingIds}
|
||||||
|
selectedOpeningIds={selectedOpeningIds}
|
||||||
|
selectedStairIds={selectedStairIds}
|
||||||
|
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
selectedRoomIds={selectedRoomIds}
|
||||||
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedContextObjectIds={selectedContextObjectIds}
|
||||||
|
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
|
||||||
|
roomStamp={roomStamp}
|
||||||
|
onStampMove={onStampMove}
|
||||||
|
onStampEdit={onStampEdit}
|
||||||
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
|
onSelect={onPlanSelect}
|
||||||
|
onMarquee={onPlanMarquee}
|
||||||
|
onSegmentCut={onSegmentCut}
|
||||||
|
onContextMenu={onPlanContextMenu}
|
||||||
|
activeTool={activeTool}
|
||||||
|
toolInputActive={toolInputActive}
|
||||||
|
toolHandlers={toolHandlers}
|
||||||
|
hudFields={hudFields}
|
||||||
|
grips={grips}
|
||||||
|
edgeGrips={edgeGrips}
|
||||||
|
hairline={hairline}
|
||||||
|
marqueeColor={marqueeColor}
|
||||||
|
snapColor={snapColor}
|
||||||
|
selectedDrawingId={selectedDrawingId}
|
||||||
|
selectedDrawingIds={selectedDrawingIds}
|
||||||
|
gripHandlers={gripHandlers}
|
||||||
|
/>
|
||||||
|
<div className="plan-legend">{caption}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnitt-/Ansichts-Ebene: der render3d-Schnitt-Extraktor (WASM, plan/toSection)
|
||||||
|
* durchschneidet das Modell entlang der Schnittlinie der Ebene und übersetzt das
|
||||||
|
* Ergebnis (Cut-Polygone + sichtbare/verdeckte Kanten) über generateSectionPlan
|
||||||
|
* in Plan-Primitive, die dieselbe PlanView wie ein Grundriss zeichnet.
|
||||||
|
*
|
||||||
|
* Die Berechnung ist asynchron (WASM lazy geladen); bis das Ergebnis da ist,
|
||||||
|
* bzw. wenn die Ebene keine Schnittlinie trägt oder das WASM-Modul den Export
|
||||||
|
* (noch) nicht kennt, wird ein Hinweis eingeblendet.
|
||||||
|
*/
|
||||||
|
function SectionPlanView({
|
||||||
|
project,
|
||||||
|
level,
|
||||||
|
hairline,
|
||||||
|
marqueeColor,
|
||||||
|
snapColor,
|
||||||
|
mono,
|
||||||
|
planViewRef,
|
||||||
|
onCursor,
|
||||||
|
onScale,
|
||||||
|
selectedWallIds,
|
||||||
|
selectedCeilingIds,
|
||||||
|
selectedOpeningIds,
|
||||||
|
selectedStairIds,
|
||||||
|
selectedExtrudedSolidIds,
|
||||||
|
selectedColumnIds,
|
||||||
|
selectedRoomIds,
|
||||||
|
selectedRoofIds,
|
||||||
|
selectedSectionLineId,
|
||||||
|
roomStamp,
|
||||||
|
onStampMove,
|
||||||
|
onStampEdit,
|
||||||
|
onEditDrawingText,
|
||||||
|
onOpenSectionLevel,
|
||||||
|
onPlanSelect,
|
||||||
|
onPlanMarquee,
|
||||||
|
onSegmentCut,
|
||||||
|
onPlanContextMenu,
|
||||||
|
activeTool,
|
||||||
|
toolInputActive,
|
||||||
|
toolHandlers,
|
||||||
|
hudFields,
|
||||||
|
grips,
|
||||||
|
edgeGrips,
|
||||||
|
selectedDrawingId,
|
||||||
|
selectedDrawingIds,
|
||||||
|
gripHandlers,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
level: DrawingLevel;
|
||||||
|
hairline: boolean;
|
||||||
|
marqueeColor: string;
|
||||||
|
snapColor: string;
|
||||||
|
mono: boolean;
|
||||||
|
planViewRef: React.RefObject<PlanViewHandle>;
|
||||||
|
onCursor: (model: Vec2 | null) => void;
|
||||||
|
onScale: (n: number) => void;
|
||||||
|
selectedWallIds: string[];
|
||||||
|
selectedCeilingIds: string[];
|
||||||
|
selectedOpeningIds: string[];
|
||||||
|
selectedStairIds: string[];
|
||||||
|
selectedExtrudedSolidIds: string[];
|
||||||
|
selectedColumnIds: string[];
|
||||||
|
selectedRoomIds: string[];
|
||||||
|
selectedRoofIds: string[];
|
||||||
|
selectedSectionLineId: string | null;
|
||||||
|
roomStamp: { roomId: string; anchor: Vec2 } | null;
|
||||||
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
|
onStampEdit: (roomId: string) => void;
|
||||||
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
|
onSegmentCut: (drawingId: string, model: Vec2) => void;
|
||||||
|
onPlanContextMenu: (info: { clientX: number; clientY: number; model: Vec2 }) => void;
|
||||||
|
activeTool: ToolId;
|
||||||
|
toolInputActive: boolean;
|
||||||
|
toolHandlers: ToolHandlers;
|
||||||
|
hudFields: HudFieldsState | null;
|
||||||
|
grips: Vec2[];
|
||||||
|
edgeGrips: EdgeGrip[];
|
||||||
|
selectedDrawingId: string | null;
|
||||||
|
selectedDrawingIds?: string[];
|
||||||
|
gripHandlers: GripHandlers;
|
||||||
|
}) {
|
||||||
|
// Die Ansicht (kind "elevation") läuft NICHT über den WASM-Schnitt, sondern
|
||||||
|
// über den rein TS-seitigen Flächen-Generator (Painter, toElevation.ts) —
|
||||||
|
// synchron, kein Lazy-Load. Nur der echte Schnitt braucht die WASM-Pipeline.
|
||||||
|
const isElevation = level.kind === "elevation";
|
||||||
|
const [output, setOutput] = useState<SectionOutput | null>(null);
|
||||||
|
const [status, setStatus] = useState<"loading" | "ready" | "empty" | "error">(
|
||||||
|
"loading",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isElevation) return; // Ansicht: kein WASM-Schnitt (synchroner Generator).
|
||||||
|
let disposed = false;
|
||||||
|
setStatus("loading");
|
||||||
|
setOutput(null);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const out = await computeSection(project, level);
|
||||||
|
if (disposed) return;
|
||||||
|
if (!out) {
|
||||||
|
setStatus("empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOutput(out);
|
||||||
|
setStatus("ready");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("render3d-Schnitt fehlgeschlagen:", e);
|
||||||
|
if (!disposed) setStatus("error");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [project, level, isElevation]);
|
||||||
|
|
||||||
|
// Ansichts-Plan: gefüllte Fassadenansicht inkl. optionalem Schatten
|
||||||
|
// (level.shadows). Liefert null, wenn die Ebene keine Ansichtslinie trägt.
|
||||||
|
const elevationPlan = useMemo(
|
||||||
|
() => (isElevation ? generateElevationPlan(project, level, { mono }) : null),
|
||||||
|
[isElevation, project, level, mono],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sectionPlan = useMemo(
|
||||||
|
() =>
|
||||||
|
!isElevation && output
|
||||||
|
? generateSectionPlan(output, mono, { hiddenLines: level.hiddenLines })
|
||||||
|
: null,
|
||||||
|
[isElevation, output, mono, level.hiddenLines],
|
||||||
|
);
|
||||||
|
const plan = isElevation ? elevationPlan : sectionPlan;
|
||||||
|
const effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status;
|
||||||
|
|
||||||
|
const kindLabel = level.kind === "section" ? t("stub.section") : t("stub.elevation");
|
||||||
|
|
||||||
|
if (!plan || effStatus !== "ready") {
|
||||||
|
const note =
|
||||||
|
effStatus === "empty"
|
||||||
|
? t("section.empty", { kind: kindLabel })
|
||||||
|
: effStatus === "error"
|
||||||
|
? t("section.error", { kind: kindLabel })
|
||||||
|
: t("section.loading", { kind: kindLabel });
|
||||||
|
return (
|
||||||
|
<div className="placeholder">
|
||||||
|
<strong>{level.name}</strong>
|
||||||
|
<span>{note}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="pane">
|
||||||
|
<div className="pane-title">
|
||||||
|
{t("content.sectionTitle", { name: level.name, kind: kindLabel })}
|
||||||
|
</div>
|
||||||
|
<PlanView
|
||||||
|
ref={planViewRef}
|
||||||
|
plan={plan}
|
||||||
|
resetKey={level.id}
|
||||||
|
onCursor={onCursor}
|
||||||
|
onScale={onScale}
|
||||||
|
selectedWallIds={selectedWallIds}
|
||||||
|
selectedCeilingIds={selectedCeilingIds}
|
||||||
|
selectedOpeningIds={selectedOpeningIds}
|
||||||
|
selectedStairIds={selectedStairIds}
|
||||||
|
selectedExtrudedSolidIds={selectedExtrudedSolidIds}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
selectedRoomIds={selectedRoomIds}
|
||||||
|
selectedRoofIds={selectedRoofIds}
|
||||||
|
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
|
||||||
|
roomStamp={roomStamp}
|
||||||
|
onStampMove={onStampMove}
|
||||||
|
onStampEdit={onStampEdit}
|
||||||
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
|
onSelect={onPlanSelect}
|
||||||
|
onMarquee={onPlanMarquee}
|
||||||
|
onSegmentCut={onSegmentCut}
|
||||||
|
onContextMenu={onPlanContextMenu}
|
||||||
|
activeTool={activeTool}
|
||||||
|
toolInputActive={toolInputActive}
|
||||||
|
toolHandlers={toolHandlers}
|
||||||
|
hudFields={hudFields}
|
||||||
|
grips={grips}
|
||||||
|
edgeGrips={edgeGrips}
|
||||||
|
hairline={hairline}
|
||||||
|
marqueeColor={marqueeColor}
|
||||||
|
snapColor={snapColor}
|
||||||
|
selectedDrawingId={selectedDrawingId}
|
||||||
|
selectedDrawingIds={selectedDrawingIds}
|
||||||
|
gripHandlers={gripHandlers}
|
||||||
|
/>
|
||||||
|
<div className="plan-legend">{t("section.legend", { kind: kindLabel })}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user