Schnittebenen 2D↔3D Phase 1+2, native Fenster, Hell/Dunkel-Theme, SWISSIMAGE-Import
- Schnittebenen: 3D-Live-Schnitt folgt der gewählten Grundriss-Schnittlinie (section3dCutId/sectionPlaneFromLevel), Schalter "Im 3D schneiden" im Objekt-Info, Doppelklick auf Schnittlinie springt in 2D-Schnittansicht, unsichtbare Ebenen blenden ihre Führungslinie aus - Eigene native Tauri-Fenster für Kontext-Import/Zeichnungsebenen/ Ebenen-Einstellungen/Ressourcen/Einstellungen + klassische Menüleiste (AppMenuBar) neben der Wortmarke - Hell/Dunkel-Umschalter (Einstellungen → Darstellung), persistiert, flackerfrei vor erstem Render gesetzt - SWISSIMAGE-Luftbild-Import (swisstopo WMS) als Kontext-Hintergrundebene - UI-Politur: Werkzeug-Panel Symbole/Liste umschaltbar, Topbar-Quick-Access- Icons entfernt, Zahnrad→Einstellungen in Panel-Köpfen, Footerbar/ Snap-Marker/Maß-HUD auf helle Pillen-Sprache umgestellt - Neues Dachziegel-Material (RoofingTiles013A)
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Klassische Menüleiste (Bearbeiten/Ansicht/Fenster) neben der Wortmarke in
|
||||
// der TopBar — ergänzt die bestehende Quick-Access-Icon-Leiste um textuelle
|
||||
// Menüs im Rhino-/Desktop-Stil. Reine Darstellung + Callbacks (voll gesteuert,
|
||||
// wie die übrige TopBar); jeder Eintrag ruft eine bereits existierende
|
||||
// App.tsx-Funktion auf (kein Dummy-/Platzhalter-Eintrag).
|
||||
//
|
||||
// Bezeichner englisch, UI-Text über i18n (CONVENTIONS.md).
|
||||
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import type { DropdownItem } from "./Dropdown";
|
||||
import { t } from "../i18n";
|
||||
import type { View3d, ViewType } from "./TopBar";
|
||||
|
||||
export interface AppMenuBarProps {
|
||||
/** Bearbeiten: Undo/Redo (Store) + Löschen der aktuellen Auswahl. */
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onDeleteSelection: () => void;
|
||||
hasSelection: boolean;
|
||||
|
||||
/** Ansicht: Grundriss/Perspektive-Umschalter, 3D-Blickwinkel-Presets,
|
||||
* Referenzlinien-Toggle. */
|
||||
viewType: ViewType;
|
||||
onViewType: (v: ViewType) => void;
|
||||
view3d: View3d;
|
||||
onView3d: (v: View3d) => void;
|
||||
referenceLines: boolean;
|
||||
onReferenceLines: (v: boolean) => void;
|
||||
|
||||
/** Fenster: Ressourcen-Fenster + Einstellungen (bestehende Panels). */
|
||||
resourcesOpen: boolean;
|
||||
onToggleResources: () => void;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
/** Menüleiste (Bearbeiten/Ansicht/Fenster) — sitzt in der TopBar neben der Wortmarke. */
|
||||
export function AppMenuBar({
|
||||
onUndo,
|
||||
onRedo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onDeleteSelection,
|
||||
hasSelection,
|
||||
viewType,
|
||||
onViewType,
|
||||
view3d,
|
||||
onView3d,
|
||||
referenceLines,
|
||||
onReferenceLines,
|
||||
resourcesOpen,
|
||||
onToggleResources,
|
||||
onOpenSettings,
|
||||
}: AppMenuBarProps) {
|
||||
const editItems: DropdownItem[] = [
|
||||
{ label: t("menu.edit.undo"), onSelect: onUndo, disabled: !canUndo },
|
||||
{ label: t("menu.edit.redo"), onSelect: onRedo, disabled: !canRedo },
|
||||
{ divider: true },
|
||||
{
|
||||
label: t("menu.edit.delete"),
|
||||
onSelect: onDeleteSelection,
|
||||
disabled: !hasSelection,
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
const VIEW3D_PRESETS: [View3d, string][] = [
|
||||
["front", t("view3d.front")],
|
||||
["back", t("view3d.back")],
|
||||
["side", t("view3d.side")],
|
||||
["left", t("view3d.left")],
|
||||
["top", t("view3d.top")],
|
||||
["iso", t("view3d.iso")],
|
||||
["perspective", t("view3d.perspective")],
|
||||
];
|
||||
const viewItems: DropdownItem[] = [
|
||||
{
|
||||
label: t("menu.view.floorplan"),
|
||||
onSelect: () => onViewType("grundriss"),
|
||||
active: viewType === "grundriss",
|
||||
},
|
||||
{
|
||||
label: t("menu.view.perspective"),
|
||||
onSelect: () => onViewType("perspektive"),
|
||||
active: viewType === "perspektive",
|
||||
},
|
||||
{ divider: true },
|
||||
...VIEW3D_PRESETS.map(([id, label]) => ({
|
||||
label,
|
||||
onSelect: () => onView3d(id),
|
||||
active: view3d === id,
|
||||
disabled: viewType !== "perspektive",
|
||||
})),
|
||||
{ divider: true },
|
||||
{
|
||||
label: t("menu.view.referenceLines"),
|
||||
onSelect: () => onReferenceLines(!referenceLines),
|
||||
active: referenceLines,
|
||||
},
|
||||
];
|
||||
|
||||
const windowItems: DropdownItem[] = [
|
||||
{
|
||||
label: t("menu.window.resources"),
|
||||
onSelect: onToggleResources,
|
||||
active: resourcesOpen,
|
||||
},
|
||||
{ divider: true },
|
||||
{ label: t("menu.window.settings"), onSelect: onOpenSettings },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="tb-menubar" role="menubar">
|
||||
<Dropdown
|
||||
items={editItems}
|
||||
placeholder={t("menu.edit")}
|
||||
title={t("menu.edit")}
|
||||
triggerClassName="tb-menubar-item"
|
||||
/>
|
||||
<Dropdown
|
||||
items={viewItems}
|
||||
placeholder={t("menu.view")}
|
||||
title={t("menu.view")}
|
||||
triggerClassName="tb-menubar-item"
|
||||
/>
|
||||
<Dropdown
|
||||
items={windowItems}
|
||||
placeholder={t("menu.window")}
|
||||
title={t("menu.window")}
|
||||
triggerClassName="tb-menubar-item"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+187
-31
@@ -17,13 +17,15 @@ import { fetchBuildings3d } from "../io/swissBuildings3d";
|
||||
import type { BuildingsVersion } from "../io/swissBuildings3d";
|
||||
import { fetchTerrainXyz } from "../io/swissAlti3d";
|
||||
import type { TerrainResolution } from "../io/swissAlti3d";
|
||||
import { fetchSwissImage } from "../io/swissImage";
|
||||
import { fetchOsm } from "../io/osm";
|
||||
import type { OsmSelection } from "../io/osm";
|
||||
import { featuresToContextObjects } from "../io/geoContext";
|
||||
import type { GeoFeature } from "../io/geoContext";
|
||||
import { terrainMeshFromGrid } from "../model/terrain";
|
||||
import type { GeoOrigin } from "../io/lv95";
|
||||
import type { ContextObject } from "../model/types";
|
||||
import { makeOrigin } from "../io/lv95";
|
||||
import type { GeoOrigin, LV95 } from "../io/lv95";
|
||||
import type { ContextObject, TerrainMesh } from "../model/types";
|
||||
|
||||
/** Gebäude-Import-Modus: aus, vereinfachte Box (schnell, alt) oder echte
|
||||
* swissBUILDINGS3D-Geometrie (Wände+Dach) in einer der zwei Generationen. */
|
||||
@@ -33,6 +35,8 @@ export type SwissBuildingsMode = "off" | "flach" | "v2" | "v3";
|
||||
* LV95-Koordinate (s. `Project.geoAnchor`). */
|
||||
export interface GeoAnchor {
|
||||
lv95: { e: number; n: number };
|
||||
/** Reale Höhe (m ü. M.) des Bezugspunkts — informativ, s. Project.geoAnchor. */
|
||||
height?: number;
|
||||
model: { x: number; y: number };
|
||||
label?: string;
|
||||
}
|
||||
@@ -45,11 +49,21 @@ export interface ContextImportDialogProps {
|
||||
geoAnchor?: GeoAnchor;
|
||||
/** Setzt/ersetzt (`undefined` löscht) den Georeferenzierungs-Bezug. */
|
||||
onSetGeoAnchor: (anchor: GeoAnchor | undefined) => void;
|
||||
/**
|
||||
* Läuft in einem EIGENEN nativen Fenster (siehe native/contextImportWindow.ts)
|
||||
* statt als abgedunkelter Overlay-Dialog im Hauptfenster — kein Backdrop,
|
||||
* füllt die Fensterfläche, Kopfzeile wird zum Ziehbereich statt Schliessen-
|
||||
* Knopf (Abbrechen/Importieren im Fuss bleiben — das sind echte Aktionen,
|
||||
* kein reiner Schliessen-Button wie bei SettingsDialog).
|
||||
*/
|
||||
windowed?: boolean;
|
||||
}
|
||||
|
||||
type Sources = {
|
||||
swissBuildings: SwissBuildingsMode;
|
||||
swissTerrain: boolean;
|
||||
swissImage: boolean;
|
||||
swissImageDrape: boolean;
|
||||
osmBuildings: boolean;
|
||||
osmRoads: boolean;
|
||||
osmWater: boolean;
|
||||
@@ -64,6 +78,8 @@ const DEFAULT_SOURCES: Sources = {
|
||||
// Städten sehr grosse Kacheln liefern (s. swissBuildings3d.ts-Kommentar).
|
||||
swissBuildings: "v2",
|
||||
swissTerrain: true,
|
||||
swissImage: false,
|
||||
swissImageDrape: false,
|
||||
osmBuildings: false,
|
||||
osmRoads: true,
|
||||
osmWater: true,
|
||||
@@ -78,6 +94,7 @@ export function ContextImportDialog({
|
||||
onClose,
|
||||
geoAnchor,
|
||||
onSetGeoAnchor,
|
||||
windowed = false,
|
||||
}: ContextImportDialogProps) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -91,6 +108,14 @@ export function ContextImportDialog({
|
||||
const [candidates, setCandidates] = useState<GeocodeCandidate[]>([]);
|
||||
const [selected, setSelected] = useState<GeocodeCandidate | null>(null);
|
||||
const [radius, setRadius] = useState(200);
|
||||
// Optionaler Bezugspunkt: welche LV95-Koordinate soll im Modell auf (0,0)
|
||||
// liegen (der „neue Nullpunkt", z. B. ein amtlicher Vermessungspunkt aus dem
|
||||
// Kataster). Leer = das Suchzentrum wird Modell-(0,0) (bisheriges Verhalten).
|
||||
const [refPoint, setRefPoint] = useState<{ e: string; n: string; h: string }>({
|
||||
e: "",
|
||||
n: "",
|
||||
h: "",
|
||||
});
|
||||
const [sources, setSources] = useState<Sources>(DEFAULT_SOURCES);
|
||||
// 2 m = Default (schneller/kleinere Kacheln); 0.5 m für Detailarbeit.
|
||||
const [terrainResolution, setTerrainResolution] = useState<TerrainResolution>("2");
|
||||
@@ -129,6 +154,8 @@ export function ContextImportDialog({
|
||||
const anySource =
|
||||
sources.swissBuildings !== "off" ||
|
||||
sources.swissTerrain ||
|
||||
sources.swissImage ||
|
||||
sources.swissImageDrape ||
|
||||
sources.osmBuildings ||
|
||||
sources.osmRoads ||
|
||||
sources.osmWater ||
|
||||
@@ -144,13 +171,29 @@ export function ContextImportDialog({
|
||||
setStatus(t("ctxImport.working"));
|
||||
try {
|
||||
const center = selected.lv95;
|
||||
// Bestehenden Georeferenzierungs-Bezug wiederverwenden, statt bei jedem
|
||||
// Import einen neuen (inkonsistenten) zu berechnen — der Bezugspunkt
|
||||
// bleibt fix im Modell, nur die GESUCHTE Stelle (center/radius) steuert,
|
||||
// WOVON Daten geladen werden (s. Project.geoAnchor-Doc).
|
||||
let origin: GeoOrigin | undefined = geoAnchor
|
||||
// Optionaler Bezugspunkt (LV95): dieser Punkt wird zu Modell-(0,0). Leer
|
||||
// -> das Suchzentrum wird der Nullpunkt (bisheriges Verhalten).
|
||||
const refE = Number(refPoint.e);
|
||||
const refN = Number(refPoint.n);
|
||||
const hasRef =
|
||||
refPoint.e.trim() !== "" &&
|
||||
refPoint.n.trim() !== "" &&
|
||||
Number.isFinite(refE) &&
|
||||
Number.isFinite(refN);
|
||||
const refLv95: LV95 = hasRef ? { e: refE, n: refN } : center;
|
||||
// Optionale Höhe (m ü. M.) des Bezugspunkts — rein informativ (kein Shift).
|
||||
const refH = Number(refPoint.h);
|
||||
const hasRefH = refPoint.h.trim() !== "" && Number.isFinite(refH);
|
||||
// Der gesuchte Ort landet nahe dem Modell-Nullpunkt (Modell rechnet in
|
||||
// float32 — LV95-Koordinaten im Millionenbereich würden 2D/3D unbrauchbar
|
||||
// ungenau darstellen). Der Bezug (geoAnchor) merkt sich „Modell-(0,0) =
|
||||
// dieser LV95-Punkt", damit der IFC-/DXF-Export trotzdem die echten
|
||||
// Landeskoordinaten schreibt und weitere Importe lagerichtig dazu liegen.
|
||||
// Mit „Neuer Bezugspunkt" (georef) lässt sich der Ursprung später auf einen
|
||||
// beliebigen Modellpunkt verschieben.
|
||||
let origin: GeoOrigin = geoAnchor
|
||||
? { e: geoAnchor.lv95.e - geoAnchor.model.x, n: geoAnchor.lv95.n - geoAnchor.model.y }
|
||||
: undefined;
|
||||
: makeOrigin(refLv95);
|
||||
const all: GeoFeature[] = [];
|
||||
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
||||
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
||||
@@ -200,8 +243,10 @@ export function ContextImportDialog({
|
||||
}
|
||||
|
||||
// Gelände-Höhenraster (echtes swissALTI3D-XYZ, native Auflösung) → Terrain-Mesh.
|
||||
let terrain: ContextObject | null = null;
|
||||
if (sources.swissTerrain) {
|
||||
// Die Drapierung braucht das TIN -> Terrain auch dann holen, wenn nur
|
||||
// „Luftbild aufs Gelände" (ohne eigenen Terrain-Haken) gewählt wäre.
|
||||
let terrain: TerrainMesh | null = null;
|
||||
if (sources.swissTerrain || sources.swissImageDrape) {
|
||||
const r = await fetchTerrainXyz(center, radius, origin, terrainResolution);
|
||||
origin = r.origin;
|
||||
if (r.grid) {
|
||||
@@ -210,7 +255,30 @@ export function ContextImportDialog({
|
||||
}
|
||||
}
|
||||
|
||||
if (all.length === 0 && !terrain && extraObjs.length === 0) {
|
||||
// SWISSIMAGE-Luftbild (WMS) → georeferenziertes Rasterbild (EIN ContextImage).
|
||||
// • `swissImage` → 2D-Grundriss-Hintergrund.
|
||||
// • `swissImageDrape` → `drape:true`: der 3D-Viewer legt es pixelscharf als
|
||||
// Textur aufs Gelände (render3d `set_aerial_texture`).
|
||||
// Beide Modi teilen dasselbe Bild; das Bild wird nur EINMAL geladen.
|
||||
let image: ContextObject | null = null;
|
||||
if (sources.swissImage || sources.swissImageDrape) {
|
||||
const r = await fetchSwissImage(center, radius, origin);
|
||||
origin = r.origin;
|
||||
if (r.dataUrl) {
|
||||
image = {
|
||||
id: `img-${Date.now()}`,
|
||||
type: "image",
|
||||
name: `Luftbild ${selected.label}`,
|
||||
layer: "Luftbild",
|
||||
src: r.dataUrl,
|
||||
min: r.min,
|
||||
max: r.max,
|
||||
drape: sources.swissImageDrape,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (all.length === 0 && !terrain && !image && extraObjs.length === 0) {
|
||||
setError(
|
||||
buildingsSkipped > 0
|
||||
? t("ctxImport.buildingsSkipped", { count: buildingsSkipped })
|
||||
@@ -220,16 +288,26 @@ export function ContextImportDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Erster Import in diesem Projekt (noch kein Bezug) -> DIESEN Lauf als
|
||||
// Georeferenzierungs-Bezug speichern (Modell-(0,0) = der gesuchte Ort),
|
||||
// damit künftige Importe lagerichtig zueinander bleiben.
|
||||
if (!geoAnchor && origin) {
|
||||
onSetGeoAnchor({ lv95: { e: origin.e, n: origin.n }, model: { x: 0, y: 0 }, label: selected.label });
|
||||
// Erster Import in diesem Projekt (noch kein Bezug) -> Bezug speichern:
|
||||
// Modell-(0,0) entspricht dem gesuchten Ort (LV95 `center`). Damit landet
|
||||
// der Import nahe dem Ursprung (gut darstellbar) UND der Modell-Export
|
||||
// kann später auf die echten LV95-Koordinaten zurückrechnen.
|
||||
if (!geoAnchor) {
|
||||
const anchorLabel = hasRef
|
||||
? t("ctxImport.refPoint.anchorLabel", { e: Math.round(refE), n: Math.round(refN) })
|
||||
: selected.label;
|
||||
onSetGeoAnchor({
|
||||
lv95: refLv95,
|
||||
height: hasRefH ? refH : undefined,
|
||||
model: { x: 0, y: 0 },
|
||||
label: anchorLabel,
|
||||
});
|
||||
}
|
||||
|
||||
const objs = featuresToContextObjects(all, selected.label);
|
||||
objs.push(...extraObjs);
|
||||
if (terrain) objs.push(terrain);
|
||||
if (image) objs.push(image);
|
||||
onImport(objs);
|
||||
setStatus(
|
||||
buildingsSkipped > 0
|
||||
@@ -252,32 +330,47 @@ export function ContextImportDialog({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imp-overlay" onClick={onClose}>
|
||||
const dialog = (
|
||||
<div
|
||||
className="imp-dialog"
|
||||
className={`imp-dialog${windowed ? " imp-dialog-embedded" : ""}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("ctxImport.title")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={windowed ? undefined : (e) => e.stopPropagation()}
|
||||
>
|
||||
<header className="imp-head">
|
||||
<header
|
||||
className="imp-head"
|
||||
{...(windowed ? { "data-tauri-drag-region": true } : {})}
|
||||
>
|
||||
<span className="imp-head-title">{t("ctxImport.title")}</span>
|
||||
<button
|
||||
className="imp-close"
|
||||
onClick={onClose}
|
||||
aria-label={t("ctxImport.close")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{!windowed && (
|
||||
<button
|
||||
className="imp-close"
|
||||
onClick={onClose}
|
||||
aria-label={t("ctxImport.close")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="imp-body">
|
||||
{/* Georeferenzierungs-Bezug: sobald gesetzt, teilen sich ALLE Importe
|
||||
dieses Projekts denselben Modell↔LV95-Bezug (s. Project.geoAnchor). */}
|
||||
{geoAnchor && (
|
||||
dieses Projekts denselben Modell↔LV95-Bezug (s. Project.geoAnchor).
|
||||
Ohne Bezug wird ROH importiert (echte LV95-Koordinaten, weit vom
|
||||
Nullpunkt) — ein Hinweis erklärt den Folge-Schritt „Neuer
|
||||
Bezugspunkt". */}
|
||||
{geoAnchor ? (
|
||||
<div className="cim-geo-anchor">
|
||||
<span>{t("ctxImport.geoAnchor", { label: geoAnchor.label ?? "?" })}</span>
|
||||
<span>
|
||||
{t("ctxImport.geoAnchor", { label: geoAnchor.label ?? "?" })}
|
||||
{" — "}
|
||||
{t("ctxImport.geoAnchor.coords", {
|
||||
e: geoAnchor.lv95.e.toFixed(2),
|
||||
n: geoAnchor.lv95.n.toFixed(2),
|
||||
h: typeof geoAnchor.height === "number" ? geoAnchor.height.toFixed(2) : "—",
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="imp-linklike"
|
||||
@@ -287,6 +380,8 @@ export function ContextImportDialog({
|
||||
{t("ctxImport.geoAnchor.reset")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="cim-geo-anchor cim-geo-hint">{t("ctxImport.rawHint")}</div>
|
||||
)}
|
||||
{/* Ortssuche */}
|
||||
<section className="imp-section">
|
||||
@@ -355,6 +450,45 @@ export function ContextImportDialog({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bezugspunkt (Modell-Nullpunkt): nur solange noch kein Bezug steht.
|
||||
Leer -> Suchzentrum wird (0,0); gefüllt -> dieser LV95-Punkt wird
|
||||
(0,0) (z. B. amtlicher Vermessungspunkt aus dem Kataster). */}
|
||||
{!geoAnchor && (
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("ctxImport.refPoint")}</div>
|
||||
<div className="cim-radius-row">
|
||||
<input
|
||||
className="cim-input cim-input-num"
|
||||
type="number"
|
||||
step={0.01}
|
||||
value={refPoint.e}
|
||||
placeholder={t("ctxImport.refPoint.e")}
|
||||
onChange={(e) => setRefPoint((p) => ({ ...p, e: e.target.value }))}
|
||||
aria-label={t("ctxImport.refPoint.e")}
|
||||
/>
|
||||
<input
|
||||
className="cim-input cim-input-num"
|
||||
type="number"
|
||||
step={0.01}
|
||||
value={refPoint.n}
|
||||
placeholder={t("ctxImport.refPoint.n")}
|
||||
onChange={(e) => setRefPoint((p) => ({ ...p, n: e.target.value }))}
|
||||
aria-label={t("ctxImport.refPoint.n")}
|
||||
/>
|
||||
<input
|
||||
className="cim-input cim-input-num"
|
||||
type="number"
|
||||
step={0.01}
|
||||
value={refPoint.h}
|
||||
placeholder={t("ctxImport.refPoint.h")}
|
||||
onChange={(e) => setRefPoint((p) => ({ ...p, h: e.target.value }))}
|
||||
aria-label={t("ctxImport.refPoint.h")}
|
||||
/>
|
||||
</div>
|
||||
<div className="cim-geo-hint">{t("ctxImport.refPoint.hint")}</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Quellen */}
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("ctxImport.sources")}</div>
|
||||
@@ -397,6 +531,22 @@ export function ContextImportDialog({
|
||||
<option value="0.5">{t("ctxImport.terrain.res05")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="cim-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sources.swissImage}
|
||||
onChange={() => toggle("swissImage")}
|
||||
/>
|
||||
{t("ctxImport.src.swissImage")}
|
||||
</label>
|
||||
<label className="cim-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sources.swissImageDrape}
|
||||
onChange={() => toggle("swissImageDrape")}
|
||||
/>
|
||||
{t("ctxImport.src.swissImageDrape")}
|
||||
</label>
|
||||
<label className="cim-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -473,6 +623,12 @@ export function ContextImportDialog({
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (windowed) return dialog;
|
||||
return (
|
||||
<div className="imp-overlay" onClick={onClose}>
|
||||
{dialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Beschriftetes Feld des Ebenen-/Geschoss-InlineEditors (App.tsx) — extrahiert,
|
||||
// damit das native Ebeneneinstellungs-Fenster (native/LayerSettingsWindowApp.tsx)
|
||||
// dasselbe Layout wiederverwendet, ohne den ganzen App.tsx-Modulgraph in das
|
||||
// (bewusst schlanke) Nebenfenster-Bundle zu ziehen.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function EditorField({ label, children }: { label: string; children: ReactNode }) {
|
||||
return (
|
||||
<label className="editor-field">
|
||||
<span className="editor-label">{label}</span>
|
||||
<span className="editor-control">{children}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Arbeitsumgebung-Menü (Fenster-/Dock-Layouts) — aktuelles Layout speichern,
|
||||
// benanntes Layout laden/löschen. Extrahiert aus App.tsx, damit SettingsDialog
|
||||
// es sowohl inline (Haupt-App) als auch in seinem eigenen nativen Fenster
|
||||
// (native/SettingsWindowApp.tsx) rendern kann, ohne es als vorgefertigtes
|
||||
// ReactNode über die Fenstergrenze reichen zu müssen (JSX ist nicht
|
||||
// serialisierbar). Speichern/Laden/Löschen laufen direkt über localStorage
|
||||
// (panels/layout.ts) — funktioniert in JEDEM Fenster identisch, da alle
|
||||
// Tauri-Fenster derselben App dieselbe Origin/denselben Storage teilen. Nur
|
||||
// „aktuelles Layout übernehmen" (`onApply`) muss zurück ins Fenster mit dem
|
||||
// echten App-Zustand — daher weiterhin ein Callback-Prop, kein Store-Zugriff
|
||||
// hier.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useState } from "react";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import type { DropdownItem } from "./Dropdown";
|
||||
import { PromptDialog } from "./PromptDialog";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
deleteNamedLayout,
|
||||
listNamedLayouts,
|
||||
loadNamedLayout,
|
||||
saveNamedLayout,
|
||||
} from "../panels/layout";
|
||||
import type { LayoutState } from "../panels/types";
|
||||
|
||||
export function LayoutMenu({
|
||||
layout,
|
||||
onApply,
|
||||
}: {
|
||||
layout: LayoutState;
|
||||
onApply: (layout: LayoutState) => void;
|
||||
}) {
|
||||
const [names, setNames] = useState<string[]>([]);
|
||||
// Name-Erfassung über PromptDialog statt window.prompt (im Tauri-WKWebView
|
||||
// deaktiviert — liefert sofort null, nichts würde je gespeichert).
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const refresh = () => setNames(listNamedLayouts());
|
||||
|
||||
const onSave = () => setSaving(true);
|
||||
const commitSave = (name: string) => {
|
||||
saveNamedLayout(name.trim(), layout);
|
||||
refresh();
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
// Aktions-Menü (analog ComboMenu in TopBar.tsx): „Aktuelles speichern" oben,
|
||||
// dann je eine Gruppe „Laden" und „Löschen" (Trenner statt <optgroup>, die
|
||||
// die Dropdown-Komponente nicht kennt).
|
||||
const items: DropdownItem[] = [];
|
||||
items.push({ label: t("layout.saveCurrent"), onSelect: onSave });
|
||||
if (names.length > 0) {
|
||||
items.push({ divider: true });
|
||||
for (const n of names) {
|
||||
items.push({
|
||||
label: n,
|
||||
onSelect: () => {
|
||||
const loaded = loadNamedLayout(n);
|
||||
if (loaded) onApply(loaded);
|
||||
},
|
||||
});
|
||||
}
|
||||
items.push({ divider: true });
|
||||
for (const n of names) {
|
||||
items.push({
|
||||
label: `✕ ${n}`,
|
||||
danger: true,
|
||||
onSelect: () => {
|
||||
deleteNamedLayout(n);
|
||||
refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span onPointerDownCapture={refresh}>
|
||||
<Dropdown items={items} placeholder={t("layout.placeholder")} title={t("layout.title")} />
|
||||
{saving && (
|
||||
<PromptDialog
|
||||
title={t("layout.title")}
|
||||
label={t("layout.savePrompt")}
|
||||
onConfirm={commitSave}
|
||||
onClose={() => setSaving(false)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+56
-23
@@ -2393,6 +2393,14 @@ function WindowStylesTab({
|
||||
/>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow label={t("resources.field.innerLayerRecess")}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!win.innerLayerRecess}
|
||||
onChange={(e) => patch({ innerLayerRecess: e.target.checked })}
|
||||
title={t("resources.field.innerLayerRecess.hint")}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow label={t("resources.field.transomHeight")}>
|
||||
<NumberField
|
||||
value={win.transomHeight ?? 0}
|
||||
@@ -3114,12 +3122,20 @@ export function ResourceManager({
|
||||
onClose,
|
||||
handlers,
|
||||
initialTab,
|
||||
windowed = false,
|
||||
}: {
|
||||
project: Project;
|
||||
onClose: () => void;
|
||||
handlers: ResourceManagerHandlers;
|
||||
/** Beim Öffnen (bzw. bei erneutem Sprung) direkt aktiver Tab, z. B. „doorStyles". */
|
||||
initialTab?: TabId;
|
||||
/**
|
||||
* Läuft in einem EIGENEN nativen Fenster (siehe native/resourceWindow.ts)
|
||||
* statt als schwebendes Overlay im Hauptfenster: füllt die Fensterfläche
|
||||
* komplett (kein eigenes Verschieben/Grössenändern per JS — das übernimmt
|
||||
* das OS-Fenster), Kopfzeile bleibt nur als Tab-Träger ohne Drag-Cursor.
|
||||
*/
|
||||
windowed?: boolean;
|
||||
}) {
|
||||
const [tab, setTab] = useState<TabId>(initialTab ?? "components");
|
||||
// Erneuter Sprung aus einem Element heraus (z. B. „Typ bearbeiten" an einer
|
||||
@@ -3185,30 +3201,44 @@ export function ResourceManager({
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="res-window"
|
||||
className={`res-window${windowed ? " res-window-embedded" : ""}`}
|
||||
role="dialog"
|
||||
aria-label={t("resources.label")}
|
||||
style={{
|
||||
left: `${rect.x}px`,
|
||||
top: `${rect.y}px`,
|
||||
width: `${rect.w}px`,
|
||||
height: `${rect.h}px`,
|
||||
}}
|
||||
style={
|
||||
windowed
|
||||
? undefined
|
||||
: {
|
||||
left: `${rect.x}px`,
|
||||
top: `${rect.y}px`,
|
||||
width: `${rect.w}px`,
|
||||
height: `${rect.h}px`,
|
||||
}
|
||||
}
|
||||
>
|
||||
<header className="res-head" onPointerDown={onTitlePointerDown}>
|
||||
<header
|
||||
className="res-head"
|
||||
onPointerDown={windowed ? undefined : onTitlePointerDown}
|
||||
// Im eigenen nativen Fenster (randlos, s. BORDERLESS_WINDOW_CHROME)
|
||||
// ist diese Kopfzeile der einzige Ziehbereich — genau wie die
|
||||
// TopBar des Hauptfensters (`data-tauri-drag-region`). Ohne OS-
|
||||
// Titelzeile bleibt so trotzdem EIN Griff zum Verschieben.
|
||||
{...(windowed ? { "data-tauri-drag-region": true } : {})}
|
||||
>
|
||||
<div className="res-head-title">
|
||||
<span className="res-head-icon">
|
||||
<EyeIcon open />
|
||||
</span>
|
||||
{t("resources.title")}
|
||||
</div>
|
||||
<button
|
||||
className="res-close"
|
||||
onClick={onClose}
|
||||
aria-label={t("resources.close")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{!windowed && (
|
||||
<button
|
||||
className="res-close"
|
||||
onClick={onClose}
|
||||
aria-label={t("resources.close")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<nav className="res-tabs" role="tablist">
|
||||
@@ -3309,14 +3339,17 @@ export function ResourceManager({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Greifer unten rechts (Größe ändern). */}
|
||||
<div
|
||||
className="floating-resize res-resize"
|
||||
role="separator"
|
||||
aria-label={t("dock.resizePanel")}
|
||||
title={t("dock.resizePanel")}
|
||||
onPointerDown={onResizePointerDown}
|
||||
/>
|
||||
{/* Greifer unten rechts (Größe ändern) — im eigenen OS-Fenster übernimmt
|
||||
das Betriebssystem das Grössenändern, daher windowed ohne Greifer. */}
|
||||
{!windowed && (
|
||||
<div
|
||||
className="floating-resize res-resize"
|
||||
role="separator"
|
||||
aria-label={t("dock.resizePanel")}
|
||||
title={t("dock.resizePanel")}
|
||||
onPointerDown={onResizePointerDown}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
+79
-30
@@ -10,20 +10,26 @@
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { ACCENT_SWATCHES } from "../theme/accents";
|
||||
import type { Project } from "../model/types";
|
||||
import { ColorHexField } from "./ColorHexField";
|
||||
import { LayoutMenu } from "./LayoutMenu";
|
||||
import type { RendererMode } from "./StatusBar";
|
||||
import type { LayoutState } from "../panels/types";
|
||||
import type { ThemeMode } from "../theme/themeMode";
|
||||
|
||||
export interface SettingsDialogProps {
|
||||
/** Hell/Dunkel — Standard ist Hell (siehe theme/themeMode.ts). */
|
||||
themeMode: ThemeMode;
|
||||
onThemeModeChange: (mode: ThemeMode) => void;
|
||||
marqueeColor: string;
|
||||
onMarqueeColorChange: (hex: string) => void;
|
||||
snapColor: string;
|
||||
onSnapColorChange: (hex: string) => void;
|
||||
project: Project;
|
||||
setProject: (next: Project | ((p: Project) => Project)) => void;
|
||||
/** Referenzhöhe des Erdgeschosses (m ü. M.) — statt des ganzen Projekts nur
|
||||
* dieses eine Feld + sein Setter (schmale Grenzfläche, s. `windowed`). */
|
||||
referenceElevationMasl: number | undefined;
|
||||
onReferenceElevationChange: (n: number | undefined) => void;
|
||||
/** Aktuell gewählter Plan-Renderer (aus dem `?engine`-URL-Param abgeleitet). */
|
||||
rendererMode: RendererMode;
|
||||
/** Ist `navigator.gpu` vorhanden? Steuert, ob „Nordstern" wählbar ist. */
|
||||
@@ -33,23 +39,37 @@ export interface SettingsDialogProps {
|
||||
* Live-Remount der Renderer-Hooks).
|
||||
*/
|
||||
onRendererModeChange: (mode: RendererMode) => void;
|
||||
/** Arbeitsumgebung-Auswahl (Fenster-/Dock-Layouts) — früher in der TopBar. */
|
||||
layoutMenu: ReactNode;
|
||||
/** Arbeitsumgebung (Fenster-/Dock-Layouts) — LayoutMenu wird HIER gerendert
|
||||
* (nicht mehr als vorgefertigtes ReactNode hereingereicht: JSX ist über eine
|
||||
* Fenstergrenze nicht serialisierbar, s. `windowed`). */
|
||||
layout: LayoutState;
|
||||
onApplyLayout: (layout: LayoutState) => void;
|
||||
onClose: () => void;
|
||||
/**
|
||||
* Läuft in einem EIGENEN nativen Fenster (siehe native/settingsWindow.ts)
|
||||
* statt als abgedunkelter Overlay-Dialog im Hauptfenster: kein Backdrop/
|
||||
* Klick-aussen-schliesst (kein „aussen" in einem echten OS-Fenster), füllt
|
||||
* die Fensterfläche, Kopfzeile wird zum Ziehbereich statt zum Schliessen-Klick.
|
||||
*/
|
||||
windowed?: boolean;
|
||||
}
|
||||
|
||||
export function SettingsDialog({
|
||||
themeMode,
|
||||
onThemeModeChange,
|
||||
marqueeColor,
|
||||
onMarqueeColorChange,
|
||||
snapColor,
|
||||
onSnapColorChange,
|
||||
project,
|
||||
setProject,
|
||||
referenceElevationMasl,
|
||||
onReferenceElevationChange,
|
||||
rendererMode,
|
||||
webGpuAvailable,
|
||||
onRendererModeChange,
|
||||
layoutMenu,
|
||||
layout,
|
||||
onApplyLayout,
|
||||
onClose,
|
||||
windowed = false,
|
||||
}: SettingsDialogProps) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -64,47 +84,49 @@ export function SettingsDialog({
|
||||
// gerundeten Projektwert zu schreiben. Synchronisiert sich, wenn sich das
|
||||
// Projekt von aussen ändert (z. B. Undo/Import).
|
||||
const [elevationText, setElevationText] = useState(
|
||||
project.referenceElevationMasl != null ? String(project.referenceElevationMasl) : "",
|
||||
referenceElevationMasl != null ? String(referenceElevationMasl) : "",
|
||||
);
|
||||
useEffect(() => {
|
||||
setElevationText(
|
||||
project.referenceElevationMasl != null ? String(project.referenceElevationMasl) : "",
|
||||
);
|
||||
}, [project.referenceElevationMasl]);
|
||||
setElevationText(referenceElevationMasl != null ? String(referenceElevationMasl) : "");
|
||||
}, [referenceElevationMasl]);
|
||||
|
||||
const commitElevation = (raw: string) => {
|
||||
setElevationText(raw);
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "") {
|
||||
setProject((p) => ({ ...p, referenceElevationMasl: undefined }));
|
||||
onReferenceElevationChange(undefined);
|
||||
return;
|
||||
}
|
||||
const n = Number(trimmed);
|
||||
if (Number.isFinite(n)) {
|
||||
setProject((p) => ({ ...p, referenceElevationMasl: n }));
|
||||
onReferenceElevationChange(n);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imp-overlay" onClick={onClose}>
|
||||
const dialog = (
|
||||
<div
|
||||
className="imp-dialog"
|
||||
className={`imp-dialog${windowed ? " imp-dialog-embedded" : ""}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("settings.title")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={windowed ? undefined : (e) => e.stopPropagation()}
|
||||
>
|
||||
<header className="imp-head">
|
||||
<header
|
||||
className="imp-head"
|
||||
{...(windowed ? { "data-tauri-drag-region": true } : {})}
|
||||
>
|
||||
<span className="imp-head-title">{t("settings.title")}</span>
|
||||
<button className="imp-close" onClick={onClose} aria-label={t("export.close")}>
|
||||
×
|
||||
</button>
|
||||
{!windowed && (
|
||||
<button className="imp-close" onClick={onClose} aria-label={t("export.close")}>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="imp-body">
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("settings.section.workspace")}</div>
|
||||
{layoutMenu}
|
||||
<LayoutMenu layout={layout} onApply={onApplyLayout} />
|
||||
</section>
|
||||
|
||||
<section className="imp-section">
|
||||
@@ -136,6 +158,25 @@ export function SettingsDialog({
|
||||
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("settings.section.display")}</div>
|
||||
<span className="settings-field-label">{t("settings.theme")}</span>
|
||||
<span className="sb-renderer-toggle" role="group" aria-label={t("settings.theme")}>
|
||||
<button
|
||||
type="button"
|
||||
className={`sb-renderer-btn${themeMode === "light" ? " active" : ""}`}
|
||||
aria-pressed={themeMode === "light"}
|
||||
onClick={() => onThemeModeChange("light")}
|
||||
>
|
||||
{t("settings.theme.light")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`sb-renderer-btn${themeMode === "dark" ? " active" : ""}`}
|
||||
aria-pressed={themeMode === "dark"}
|
||||
onClick={() => onThemeModeChange("dark")}
|
||||
>
|
||||
{t("settings.theme.dark")}
|
||||
</button>
|
||||
</span>
|
||||
<AccentColorField
|
||||
label={t("settings.marqueeColor")}
|
||||
hint={t("settings.marqueeColor.hint")}
|
||||
@@ -169,12 +210,20 @@ export function SettingsDialog({
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className="imp-foot">
|
||||
<button className="imp-btn primary" onClick={onClose}>
|
||||
{t("export.close")}
|
||||
</button>
|
||||
</footer>
|
||||
{!windowed && (
|
||||
<footer className="imp-foot">
|
||||
<button className="imp-btn primary" onClick={onClose}>
|
||||
{t("export.close")}
|
||||
</button>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (windowed) return dialog;
|
||||
return (
|
||||
<div className="imp-overlay" onClick={onClose}>
|
||||
{dialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+168
-638
@@ -16,10 +16,6 @@ import { Dropdown } from "./Dropdown";
|
||||
import type { DropdownItem, DropdownOption } from "./Dropdown";
|
||||
import { PromptDialog } from "./PromptDialog";
|
||||
import { WindowControls } from "./WindowControls";
|
||||
import { RibbonTabs } from "./ribbon/RibbonBar";
|
||||
import type { RibbonTabId } from "./ribbon/ribbonItems";
|
||||
import { getCommand } from "../commands/registry";
|
||||
import { CommandIcon } from "./ribbon/CommandIcon";
|
||||
import {
|
||||
applyMark,
|
||||
applyPreset,
|
||||
@@ -57,6 +53,7 @@ export type DetailLevel = "grob" | "mittel" | "fein";
|
||||
export type RenderMode =
|
||||
| "shaded"
|
||||
| "white"
|
||||
| "contour"
|
||||
| "textured"
|
||||
| "wireframe"
|
||||
| "hidden"
|
||||
@@ -105,14 +102,11 @@ const ZOOM_CLUSTER_WIDTH = 140;
|
||||
export interface TopBarProps {
|
||||
project: Project;
|
||||
|
||||
// Ribbon-Tab-Reiter (2D/3D/BIM/Ansichten) sitzen in DIESER Zeile — der Ribbon-
|
||||
// Inhalt darunter (RibbonBar). Der aktive Tab liegt in App (geteilter State).
|
||||
ribbonTab: RibbonTabId;
|
||||
onRibbonTab: (id: RibbonTabId) => void;
|
||||
|
||||
// Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab
|
||||
// gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale
|
||||
// Leiste: Marke/Ressourcen · Tabs · Text-Formatierung · Datei/Export/Einstell.
|
||||
// Kein Ribbon-Tab-Reiter mehr (Werkzeuge leben im ToolsPanel, siehe
|
||||
// ribbonItems.ts/layout.ts v12) — die TopBar ist nur noch die schmale
|
||||
// globale Leiste: Marke/Menü · Datei/Export/Einstellungen. Ansichts-/Zoom-/
|
||||
// Darstellungs-Steuerung folgt direkt darunter (siehe ViewRibbonTab, von
|
||||
// App unconditional gerendert statt über Tab-Umschaltung).
|
||||
|
||||
/** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */
|
||||
zoomEnabled: boolean;
|
||||
@@ -155,8 +149,16 @@ export interface TopBarProps {
|
||||
/** Projekt aus einer Datei laden. */
|
||||
onOpenProject: () => void;
|
||||
|
||||
// Die Text-/Font-Formatierung (TextGroup) liegt jetzt im „Text"-Ribbon-Tab
|
||||
// (App reicht sie via RibbonBar.tabContent), nicht mehr in dieser Leiste.
|
||||
/** Menüleiste (Bearbeiten/Ansicht/Fenster, {@link AppMenuBar}) — von App
|
||||
* zusammengesetzt (dort leben Undo/Redo/Auswahl/View-State), hier nur
|
||||
* platziert. `undefined` blendet sie aus (z. B. für künftige eingebettete
|
||||
* Kontexte ohne volle Chrome). */
|
||||
menuBar?: React.ReactNode;
|
||||
|
||||
/** Ansichts-/Zoom-/Darstellungs-Steuerung ({@link ViewRibbonTab}) — von App
|
||||
* geliefert (dort leben View-State/Setter), hier direkt in DERSELBEN
|
||||
* schmalen Leiste platziert (kein eigenes Band mehr, Mockup: EINE Zeile). */
|
||||
viewControls?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,18 +193,6 @@ function Sep() {
|
||||
return <span className="tb-sep" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material-Symbols-Icon für die Oberleiste (gleiche Icon-Font wie ContextMenu/
|
||||
* MenuIcon). Rein dekorativ — der sprechende Text steckt im `title`/aria-label
|
||||
* des umgebenden Buttons.
|
||||
*/
|
||||
function TbIcon({ name }: { name: string }) {
|
||||
return (
|
||||
<span className="material-symbols-outlined tb-ico" aria-hidden="true">
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kleines Kombinations-Menü (Vectorworks-Stil) — gespiegelt von `LayoutMenu`:
|
||||
@@ -288,88 +278,6 @@ function ComboMenu({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kamera-Einstellungen als kleines Popover (Vectorworks-Stil): ein FOV-Regler
|
||||
* für den Sichtwinkel der Perspektivkamera. Öffnet/schliesst per Button; ein
|
||||
* Klick ausserhalb schliesst es. Rein gesteuert über `fov`/`onFov`.
|
||||
*/
|
||||
function CameraMenu({
|
||||
fov,
|
||||
onFov,
|
||||
disabled,
|
||||
}: {
|
||||
fov: number;
|
||||
onFov: (n: number) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
// Popover-Position (fixed), aus dem Button-Rect berechnet — die Oberleiste
|
||||
// hat overflow:hidden und würde ein absolut positioniertes Popover abschneiden.
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const btnRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [open]);
|
||||
|
||||
const toggle = () => {
|
||||
const next = !open;
|
||||
if (next && btnRef.current) {
|
||||
const r = btnRef.current.getBoundingClientRect();
|
||||
setPos({ left: r.left, top: r.bottom + 6 });
|
||||
}
|
||||
setOpen(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tb-popwrap" ref={wrapRef}>
|
||||
<button
|
||||
ref={btnRef}
|
||||
className={`view-icon${open ? " active" : ""}`}
|
||||
disabled={disabled}
|
||||
onClick={toggle}
|
||||
title={`${t("view3d.camera")} — ${t("view3d.camera.hint")}`}
|
||||
aria-label={t("view3d.camera")}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<ViewIcon name="camera" />
|
||||
</button>
|
||||
{open && !disabled && (
|
||||
<div
|
||||
className="tb-popover"
|
||||
role="dialog"
|
||||
aria-label={t("view3d.camera")}
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
<label className="tb-poprow">
|
||||
<span className="tb-label">
|
||||
{t("view3d.fov")}: {Math.round(fov)}°
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={20}
|
||||
max={90}
|
||||
step={1}
|
||||
value={fov}
|
||||
onChange={(e) => onFov(Number(e.target.value))}
|
||||
title={t("view3d.fov.hint")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Die vier oberen Iso-Oktanten (untere vier bleiben im Hochbau ungenutzt). */
|
||||
const ISO_VARIANTS: View3d[] = ["iso", "isoFrontLeft", "isoBackRight", "isoBackLeft"];
|
||||
|
||||
@@ -389,27 +297,42 @@ function IsoCheck() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Die Elevations-Ansichten (Vorne/Hinten/Links/Rechts/Oben) — als EIN Symbol
|
||||
* + Dropdown zusammengefasst (statt fünf Buchstaben-Knöpfen). */
|
||||
const ELEVATION_VIEWS: View3d[] = ["front", "back", "left", "side", "top"];
|
||||
|
||||
/**
|
||||
* Iso-Oktanten als Split-Button (Vectorworks-Stil): Klick auf das Würfel-Icon
|
||||
* springt direkt auf die zuletzt gewählte Iso-Ansicht (Default „iso" =
|
||||
* vorne-oben-rechts, s. View3d); der schmale Pfeil daneben öffnet ein Popover
|
||||
* mit allen vier oberen Iso-Oktanten (untere vier bleiben im Hochbau ungenutzt).
|
||||
* Popover-Markup/Klassen wie `Dropdown`s Listenmodus (`ctx-menu`/`tb-dd-item`),
|
||||
* Positionierung wie `CameraMenu` (fixed, kein Portal — entkommt dem
|
||||
* `overflow:hidden` der Oberleiste).
|
||||
* Split-Button mit Dropdown (Vectorworks-Stil): Klick auf das Symbol springt
|
||||
* direkt auf die aktuelle/zuletzt gewählte Ansicht der Gruppe; der schmale
|
||||
* Pfeil daneben öffnet ein Popover mit allen Varianten. Generisch für die
|
||||
* „Ansichten" (Elevations) UND „Iso" (Oktanten) — beide teilen dieselbe
|
||||
* robuste Popover-Logik (fixed positioniert, entkommt dem Oberleisten-
|
||||
* Overflow; schliesst bei Aussenklick).
|
||||
*/
|
||||
function IsoMenu({
|
||||
function ViewSplitMenu({
|
||||
view3d,
|
||||
onView3d,
|
||||
viewType,
|
||||
onViewType,
|
||||
disabled,
|
||||
variants,
|
||||
fallback,
|
||||
icon,
|
||||
menuLabel,
|
||||
}: {
|
||||
view3d: View3d;
|
||||
onView3d: (v: View3d) => void;
|
||||
viewType: ViewType;
|
||||
onViewType: (v: ViewType) => void;
|
||||
disabled: boolean;
|
||||
/** Die im Dropdown wählbaren Ansichten dieser Gruppe. */
|
||||
variants: View3d[];
|
||||
/** Ziel des Symbol-Klicks, falls die aktuelle Ansicht nicht zur Gruppe gehört. */
|
||||
fallback: View3d;
|
||||
/** Symbol des Haupt-Knopfs. */
|
||||
icon: React.ReactNode;
|
||||
/** Beschriftung (aria/Tooltip) der Gruppe. */
|
||||
menuLabel: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
|
||||
@@ -427,8 +350,8 @@ function IsoMenu({
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [open]);
|
||||
|
||||
const current: View3d = ISO_VARIANTS.includes(view3d) ? view3d : "iso";
|
||||
const active = viewType === "perspektive" && ISO_VARIANTS.includes(view3d);
|
||||
const current: View3d = variants.includes(view3d) ? view3d : fallback;
|
||||
const active = viewType === "perspektive" && variants.includes(view3d);
|
||||
|
||||
const select = (v: View3d) => {
|
||||
onView3d(v);
|
||||
@@ -448,21 +371,23 @@ function IsoMenu({
|
||||
return (
|
||||
<div className="tb-popwrap view-icon-split" ref={wrapRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-icon view-icon-split-main${active ? " active" : ""}`}
|
||||
disabled={disabled}
|
||||
onClick={() => select(current)}
|
||||
title={disabled ? t("topbar.viewGroup.disabled") : `${t(`view3d.${current}`)} — ${t(`view3d.${current}.hint`)}`}
|
||||
aria-label={t("view3d.iso")}
|
||||
aria-label={menuLabel}
|
||||
>
|
||||
<ViewIcon name="iso" />
|
||||
{icon}
|
||||
</button>
|
||||
<button
|
||||
ref={caretRef}
|
||||
type="button"
|
||||
className={`view-icon-caret${open ? " active" : ""}`}
|
||||
disabled={disabled}
|
||||
onClick={toggle}
|
||||
title={disabled ? t("topbar.viewGroup.disabled") : `${t("view3d.iso.menu")} — ${t("view3d.iso.menu.hint")}`}
|
||||
aria-label={t("view3d.iso.menu")}
|
||||
title={disabled ? t("topbar.viewGroup.disabled") : menuLabel}
|
||||
aria-label={menuLabel}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
>
|
||||
@@ -470,29 +395,32 @@ function IsoMenu({
|
||||
<path d="M0 0l5 6 5-6z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && !disabled && (
|
||||
<div
|
||||
className="ctx-menu tb-dd-menu"
|
||||
role="menu"
|
||||
aria-label={t("view3d.iso.menu")}
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
{ISO_VARIANTS.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={view3d === v}
|
||||
className={`ctx-item tb-dd-item${view3d === v ? " is-active" : ""}`}
|
||||
onClick={() => select(v)}
|
||||
title={t(`view3d.${v}.hint`)}
|
||||
>
|
||||
<span className="tb-dd-check-slot">{view3d === v && <IsoCheck />}</span>
|
||||
<span className="ctx-item-label">{t(`view3d.${v}`)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{open &&
|
||||
!disabled &&
|
||||
createPortal(
|
||||
<div
|
||||
className="ctx-menu tb-dd-menu"
|
||||
role="menu"
|
||||
aria-label={menuLabel}
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
{variants.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={view3d === v}
|
||||
className={`ctx-item tb-dd-item${view3d === v ? " is-active" : ""}`}
|
||||
onClick={() => select(v)}
|
||||
title={t(`view3d.${v}.hint`)}
|
||||
>
|
||||
<span className="tb-dd-check-slot">{view3d === v && <IsoCheck />}</span>
|
||||
<span className="ctx-item-label">{t(`view3d.${v}`)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -596,6 +524,28 @@ function ViewIcon({ name }: { name: View3d | "camera" | "grundriss" }) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Symbol für die „Ansichten" (Elevations): eine Fassade — Rechteck mit Tür +
|
||||
* Bodenlinie (statt des unklaren Quadrat-mit-Steg-Icons). */
|
||||
function ElevationIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3.5" y="2.5" width="9" height="10.5" />
|
||||
<rect x="6.4" y="8" width="3.2" height="5" />
|
||||
<line x1="1.5" y1="13" x2="14.5" y2="13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Eine Zelle einer Segmentpille. */
|
||||
interface SegmentCell {
|
||||
/** Stabiler Schlüssel. */
|
||||
@@ -1059,191 +1009,10 @@ function touchedParagraphs(doc: RichTextDoc, range: TextRange | null): number[]
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei-Menü der Oberleiste (Burger, ganz rechts): Speichern/Öffnen des
|
||||
* Projekts sowie Import (DXF/DWG) und Export (PDF/DXF) — vorher eigene
|
||||
* Icon-Pillen im Zoom-Bereich, jetzt gebündelt hinter einem Menü, damit die
|
||||
* Leiste ruhiger bleibt (GTK-Burger-Stil).
|
||||
*/
|
||||
/** Ein kleiner Icon-Knopf der Quick-Access-Leiste (OCS-Stil). */
|
||||
function QaButton({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
disabled,
|
||||
active,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`tb-qa-btn${active ? " active" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-pressed={active}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
>
|
||||
<TbIcon name={icon} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** Kleiner Trenner in der Quick-Access-Leiste. */
|
||||
function QaSep() {
|
||||
return <span className="tb-qa-sep" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick-Access-Leiste (OCS-Stil) — alle früheren Datei-Burger-Aktionen plus
|
||||
* Ressourcen/Einstellungen als kleine Icon-Knöpfe direkt in der TopBar-Zeile.
|
||||
* Ersetzt das Burger-Menü (`AppMenu`). Rein gesteuert über Callbacks (i18n via t).
|
||||
*/
|
||||
/**
|
||||
* Kompakte Kamera-Gruppe der Quick-Access-Leiste: NUR das Kamera-Settings-
|
||||
* Popover (FOV), damit es immer erreichbar ist, ohne die Ansichts-Presets
|
||||
* (die bleiben unverändert im „Ansichten"-Tab und im 3D-Ribbon-Tab).
|
||||
*/
|
||||
function QaCameraGroup({
|
||||
viewType,
|
||||
fov,
|
||||
onFov,
|
||||
cameraEnabled,
|
||||
}: {
|
||||
viewType: ViewType;
|
||||
fov: number;
|
||||
onFov: (n: number) => void;
|
||||
cameraEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="tb-qa-camera" role="toolbar" aria-label={t("topbar.viewGroup")}>
|
||||
<CameraMenu
|
||||
fov={fov}
|
||||
onFov={onFov}
|
||||
disabled={!cameraEnabled || viewType !== "perspektive"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** App-Version (spiegelt src-tauri/tauri.conf.json). */
|
||||
const APP_VERSION = "0.1.0";
|
||||
|
||||
/**
|
||||
* Export-Sammelmenü: EIN „Export"-Knopf in der Quick-Access-Leiste öffnet ein
|
||||
* Dropdown (wie ein Kontextmenü) mit allen Export-Formaten — ersetzt die früher
|
||||
* einzeln aufgereihten Export-Icons. Popover fixed positioniert (die Oberleiste
|
||||
* hat overflow:hidden), Muster wie {@link CameraMenu}.
|
||||
*/
|
||||
function ExportMenu({
|
||||
onExportPdf,
|
||||
onExportDxf,
|
||||
onExportSchedule,
|
||||
onExportIfc,
|
||||
onExportObj,
|
||||
onExportStl,
|
||||
exportEnabled,
|
||||
}: {
|
||||
onExportPdf: () => void;
|
||||
onExportDxf: () => void;
|
||||
onExportSchedule: () => void;
|
||||
onExportIfc: () => void;
|
||||
onExportObj: () => void;
|
||||
onExportStl: () => void;
|
||||
exportEnabled: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const btnRef = useRef<HTMLButtonElement | null>(null);
|
||||
// Das Popover hängt per Portal an body (NICHT in wrapRef) — der Outside-Handler
|
||||
// muss es daher separat als „innen" behandeln, sonst schliesst der mousedown
|
||||
// das Menü vor dem Klick.
|
||||
const popRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (wrapRef.current?.contains(target)) return;
|
||||
if (popRef.current?.contains(target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [open]);
|
||||
|
||||
const toggle = () => {
|
||||
const next = !open;
|
||||
if (next && btnRef.current) {
|
||||
const r = btnRef.current.getBoundingClientRect();
|
||||
setPos({ left: r.left, top: r.bottom + 6 });
|
||||
}
|
||||
setOpen(next);
|
||||
};
|
||||
const pick = (fn: () => void) => {
|
||||
setOpen(false);
|
||||
fn();
|
||||
};
|
||||
|
||||
const items: { icon: string; label: string; onClick: () => void; disabled?: boolean }[] = [
|
||||
{ icon: "picture_as_pdf", label: t("file.exportPdf"), onClick: onExportPdf, disabled: !exportEnabled },
|
||||
{ icon: "output", label: t("file.exportDxf"), onClick: onExportDxf, disabled: !exportEnabled },
|
||||
{ icon: "table_view", label: t("file.exportSchedule"), onClick: onExportSchedule },
|
||||
{ icon: "deployed_code", label: t("file.exportIfc"), onClick: onExportIfc },
|
||||
{ icon: "view_in_ar", label: t("file.exportObj"), onClick: onExportObj },
|
||||
{ icon: "landscape", label: t("file.exportStl"), onClick: onExportStl },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="tb-popwrap" ref={wrapRef}>
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
className={`tb-qa-btn${open ? " active" : ""}`}
|
||||
onClick={toggle}
|
||||
title={t("file.export")}
|
||||
aria-label={t("file.export")}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<TbIcon name="ios_share" />
|
||||
</button>
|
||||
{open &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popRef}
|
||||
className="tb-popover tb-menu"
|
||||
role="menu"
|
||||
aria-label={t("file.export")}
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
{items.map((it) => (
|
||||
<button
|
||||
key={it.label}
|
||||
type="button"
|
||||
className="ctx-item"
|
||||
role="menuitem"
|
||||
disabled={it.disabled}
|
||||
onClick={() => pick(it.onClick)}
|
||||
>
|
||||
<span className="ctx-item-icon">
|
||||
<TbIcon name={it.icon} />
|
||||
</span>
|
||||
<span className="ctx-item-label">{it.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/** App-Version (spiegelt src-tauri/tauri.conf.json). Exportiert für appMenu.ts
|
||||
* (native macOS-Menüleiste, „Über Dossier"), damit die Versionsnummer nur an
|
||||
* dieser einen Stelle gepflegt wird. */
|
||||
export const APP_VERSION = "0.1.0";
|
||||
|
||||
/**
|
||||
* „Über"-Dialog (In-App, dunkel) — App-Marke, Version, Kurzbeschreibung und die
|
||||
@@ -1298,77 +1067,6 @@ function AboutDialog({ onClose }: { onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAccess({
|
||||
onOpenProject,
|
||||
onSaveProject,
|
||||
onImport,
|
||||
onExportPdf,
|
||||
onExportDxf,
|
||||
onExportSchedule,
|
||||
onExportIfc,
|
||||
onExportObj,
|
||||
onExportStl,
|
||||
onToggleResources,
|
||||
onOpenSettings,
|
||||
resourcesOpen,
|
||||
exportEnabled,
|
||||
viewType,
|
||||
fov,
|
||||
onFov,
|
||||
cameraEnabled,
|
||||
}: {
|
||||
onOpenProject: () => void;
|
||||
onSaveProject: () => void;
|
||||
onImport: () => void;
|
||||
onExportPdf: () => void;
|
||||
onExportDxf: () => void;
|
||||
onExportSchedule: () => void;
|
||||
onExportIfc: () => void;
|
||||
onExportObj: () => void;
|
||||
onExportStl: () => void;
|
||||
onToggleResources: () => void;
|
||||
onOpenSettings: () => void;
|
||||
resourcesOpen: boolean;
|
||||
exportEnabled: boolean;
|
||||
viewType: ViewType;
|
||||
fov: number;
|
||||
onFov: (n: number) => void;
|
||||
cameraEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="tb-qa" role="toolbar" aria-label={t("file.menu")}>
|
||||
<QaButton icon="folder_open" label={t("file.open")} onClick={onOpenProject} />
|
||||
<QaButton icon="save" label={t("file.save")} onClick={onSaveProject} />
|
||||
<QaSep />
|
||||
<QaButton icon="download" label={t("file.import")} onClick={onImport} />
|
||||
<ExportMenu
|
||||
onExportPdf={onExportPdf}
|
||||
onExportDxf={onExportDxf}
|
||||
onExportSchedule={onExportSchedule}
|
||||
onExportIfc={onExportIfc}
|
||||
onExportObj={onExportObj}
|
||||
onExportStl={onExportStl}
|
||||
exportEnabled={exportEnabled}
|
||||
/>
|
||||
<QaSep />
|
||||
<QaCameraGroup
|
||||
viewType={viewType}
|
||||
fov={fov}
|
||||
onFov={onFov}
|
||||
cameraEnabled={cameraEnabled}
|
||||
/>
|
||||
<QaSep />
|
||||
<QaButton
|
||||
icon="widgets"
|
||||
label={t("topbar.resources")}
|
||||
onClick={onToggleResources}
|
||||
active={resourcesOpen}
|
||||
/>
|
||||
<QaButton icon="settings" label={t("topbar.settings")} onClick={onOpenSettings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props des „Ansichten"-Tabs — die Ansichts-/Zoom-/Darstellungs-Steuerung, die
|
||||
* früher in der Mitte der TopBar sass. Eine Teilmenge von {@link TopBarProps}.
|
||||
@@ -1469,70 +1167,61 @@ export function ViewRibbonTab({
|
||||
|
||||
return (
|
||||
<div className="ribbon-views">
|
||||
{/* Ansichten als EIN kompaktes Icon-Grid (2D-Grundriss + 3D-Blicke). */}
|
||||
<div className="tb-group" role="group" aria-label={t("topbar.viewGroup")}>
|
||||
<div className="view-grid view-grid-4">
|
||||
<button
|
||||
className={`view-icon${
|
||||
viewToggleEnabled && viewType === "grundriss" ? " active" : ""
|
||||
}`}
|
||||
onClick={() => onViewType("grundriss")}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={
|
||||
viewToggleEnabled
|
||||
? t("topbar.grundriss")
|
||||
: t("topbar.viewGroup.disabled")
|
||||
}
|
||||
aria-label={t("topbar.grundriss")}
|
||||
>
|
||||
<ViewIcon name="grundriss" />
|
||||
</button>
|
||||
{(
|
||||
[
|
||||
["top", t("view3d.top")],
|
||||
["iso", t("view3d.iso")],
|
||||
["perspective", t("view3d.perspective")],
|
||||
["front", t("view3d.front")],
|
||||
["side", t("view3d.side")],
|
||||
["back", t("view3d.back")],
|
||||
["left", t("view3d.left")],
|
||||
] as [View3d, string][]
|
||||
).map(([key, label]) =>
|
||||
key === "iso" ? (
|
||||
<IsoMenu
|
||||
key={key}
|
||||
view3d={view3d}
|
||||
onView3d={onView3d}
|
||||
viewType={viewType}
|
||||
onViewType={onViewType}
|
||||
disabled={!viewToggleEnabled}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
key={key}
|
||||
className={`view-icon${
|
||||
viewToggleEnabled && viewType === "perspektive" && view3d === key
|
||||
? " active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onView3d(key);
|
||||
if (viewType !== "perspektive") onViewType("perspektive");
|
||||
}}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={
|
||||
viewToggleEnabled
|
||||
? `${label} — ${t(`view3d.${key}.hint`)}`
|
||||
: t("topbar.viewGroup.disabled")
|
||||
}
|
||||
aria-label={label}
|
||||
>
|
||||
<ViewIcon name={key} />
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Reihenfolge: Grundriss · Ansichten · Iso · Axo — ohne Trennstriche
|
||||
(eine zusammenhängende Symbol-Zeile, Nutzer-Wunsch). */}
|
||||
<span className="tb-viewtoggle" role="group" aria-label={t("topbar.viewGroup")}>
|
||||
<button
|
||||
type="button"
|
||||
className={`tb-vt-btn${
|
||||
viewToggleEnabled && viewType === "grundriss" ? " active" : ""
|
||||
}`}
|
||||
onClick={() => onViewType("grundriss")}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={viewToggleEnabled ? t("topbar.grundriss") : t("topbar.viewGroup.disabled")}
|
||||
aria-label={t("topbar.grundriss")}
|
||||
>
|
||||
<ViewIcon name="grundriss" />
|
||||
</button>
|
||||
<ViewSplitMenu
|
||||
view3d={view3d}
|
||||
onView3d={onView3d}
|
||||
viewType={viewType}
|
||||
onViewType={onViewType}
|
||||
disabled={!viewToggleEnabled}
|
||||
variants={ELEVATION_VIEWS}
|
||||
fallback="front"
|
||||
icon={<ElevationIcon />}
|
||||
menuLabel={t("topbar.views")}
|
||||
/>
|
||||
<ViewSplitMenu
|
||||
view3d={view3d}
|
||||
onView3d={onView3d}
|
||||
viewType={viewType}
|
||||
onViewType={onViewType}
|
||||
disabled={!viewToggleEnabled}
|
||||
variants={ISO_VARIANTS}
|
||||
fallback="iso"
|
||||
icon={<ViewIcon name="iso" />}
|
||||
menuLabel={t("view3d.iso.menu")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`tb-vt-btn${
|
||||
viewToggleEnabled && viewType === "perspektive" && view3d === "perspective"
|
||||
? " active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onView3d("perspective");
|
||||
if (viewType !== "perspektive") onViewType("perspektive");
|
||||
}}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={viewToggleEnabled ? `${t("topbar.axo")} — ${t("topbar.axo.hint")}` : t("topbar.viewGroup.disabled")}
|
||||
aria-label={t("topbar.axo")}
|
||||
>
|
||||
<ViewIcon name="perspective" />
|
||||
</button>
|
||||
</span>
|
||||
<Sep />
|
||||
|
||||
{/* Sichtbarkeits-Kombinationen (Ebenen-/Zeichnungskombinationen). */}
|
||||
@@ -1595,6 +1284,7 @@ export function ViewRibbonTab({
|
||||
{ value: "shaded", label: t("display.style.shaded") },
|
||||
{ value: "shaded-edges", label: t("display.style.shadedEdges") },
|
||||
{ value: "white", label: t("display.style.white") },
|
||||
{ value: "contour", label: t("display.style.contour") },
|
||||
{ value: "textured", label: t("display.style.textured") },
|
||||
{ value: "wireframe", label: t("display.style.wireframe") },
|
||||
{ value: "hidden", label: t("display.style.hidden") },
|
||||
@@ -1613,9 +1303,9 @@ export function ViewRibbonTab({
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
<Sep />
|
||||
|
||||
{/* Massstab/Zoom-Cluster (2×2). */}
|
||||
{/* Massstab/Zoom + Text-Formatierung OHNE Trennstriche, gleich gross wie
|
||||
die Dropdowns (Mockup). */}
|
||||
<div className="tb-group tb-zoomcluster" role="group" aria-label={t("topbar.scaleGroup")}>
|
||||
<div
|
||||
className={`tb-stat${
|
||||
@@ -1671,8 +1361,6 @@ export function ViewRibbonTab({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Sep />
|
||||
|
||||
|
||||
{/* Schatten ein/aus — NUR für eine aktive Ansicht (kind "elevation").
|
||||
Schreibt DrawingLevel.shadows; der Ansichts-Generator (toElevation)
|
||||
@@ -1710,149 +1398,10 @@ export function ViewRibbonTab({
|
||||
);
|
||||
}
|
||||
|
||||
export interface Ribbon3dTabProps {
|
||||
viewToggleEnabled: boolean;
|
||||
viewType: ViewType;
|
||||
onViewType: (v: ViewType) => void;
|
||||
view3d: View3d;
|
||||
onView3d: (v: View3d) => void;
|
||||
renderMode: RenderMode;
|
||||
onRenderMode: (m: RenderMode) => void;
|
||||
renderModeEnabled: boolean;
|
||||
onRunCommand: (name: string) => void;
|
||||
activeCommand: string | null;
|
||||
}
|
||||
|
||||
/** 3D-Tab des Ribbons — Kamera-Presets + Darstellungsart. Kein Duplikat des
|
||||
* Ansichten-Tabs: hier nur 3D-relevante Steuerung (Blickwinkel, Rendermodus). */
|
||||
export function Ribbon3dTab({
|
||||
viewToggleEnabled,
|
||||
viewType,
|
||||
onViewType,
|
||||
view3d,
|
||||
onView3d,
|
||||
renderMode,
|
||||
onRenderMode,
|
||||
renderModeEnabled,
|
||||
onRunCommand,
|
||||
activeCommand,
|
||||
}: Ribbon3dTabProps) {
|
||||
return (
|
||||
<div className="ribbon-views">
|
||||
<div className="tb-group" role="group" aria-label={t("topbar.viewGroup")}>
|
||||
<div className="view-grid view-grid-4">
|
||||
<button
|
||||
className={`view-icon${viewToggleEnabled && viewType === "grundriss" ? " active" : ""}`}
|
||||
onClick={() => onViewType("grundriss")}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={viewToggleEnabled ? t("topbar.grundriss") : t("topbar.viewGroup.disabled")}
|
||||
aria-label={t("topbar.grundriss")}
|
||||
>
|
||||
<ViewIcon name="grundriss" />
|
||||
</button>
|
||||
{(
|
||||
[
|
||||
["top", t("view3d.top")],
|
||||
["iso", t("view3d.iso")],
|
||||
["perspective", t("view3d.perspective")],
|
||||
["front", t("view3d.front")],
|
||||
["side", t("view3d.side")],
|
||||
["back", t("view3d.back")],
|
||||
["left", t("view3d.left")],
|
||||
] as [View3d, string][]
|
||||
).map(([key, label]) =>
|
||||
key === "iso" ? (
|
||||
<IsoMenu
|
||||
key={key}
|
||||
view3d={view3d}
|
||||
onView3d={onView3d}
|
||||
viewType={viewType}
|
||||
onViewType={onViewType}
|
||||
disabled={!viewToggleEnabled}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
key={key}
|
||||
className={`view-icon${viewToggleEnabled && viewType === "perspektive" && view3d === key ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onView3d(key);
|
||||
if (viewType !== "perspektive") onViewType("perspektive");
|
||||
}}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={viewToggleEnabled ? `${label} — ${t(`view3d.${key}.hint`)}` : t("topbar.viewGroup.disabled")}
|
||||
aria-label={label}
|
||||
>
|
||||
<ViewIcon name={key} />
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Sep />
|
||||
<div className="tb-group" role="group" aria-label={t("display.style.group")}>
|
||||
<label className="tb-field">
|
||||
<span className="tb-label">{t("display.style")}</span>
|
||||
<Dropdown
|
||||
value={renderMode}
|
||||
disabled={!renderModeEnabled}
|
||||
onChange={(v) => onRenderMode(v as RenderMode)}
|
||||
title={t("display.style.hint")}
|
||||
options={[
|
||||
{ value: "shaded", label: t("display.style.shaded") },
|
||||
{ value: "shaded-edges", label: t("display.style.shadedEdges") },
|
||||
{ value: "white", label: t("display.style.white") },
|
||||
{ value: "textured", label: t("display.style.textured") },
|
||||
{ value: "wireframe", label: t("display.style.wireframe") },
|
||||
{ value: "hidden", label: t("display.style.hidden") },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Sep />
|
||||
<div className="tb-group" role="group" aria-label={t("cmd.extrude.label")}>
|
||||
{(() => {
|
||||
const cmd = getCommand("extrude");
|
||||
const label = cmd ? t(cmd.labelKey) : "extrude";
|
||||
const active = activeCommand === "extrude";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`ribbon-btn${active ? " active" : ""}`}
|
||||
disabled={!viewToggleEnabled}
|
||||
title={viewToggleEnabled ? label : t("tool.floorOnlyDisabled")}
|
||||
onClick={() => onRunCommand("extrude")}
|
||||
>
|
||||
<CommandIcon name="extrude" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
project,
|
||||
ribbonTab,
|
||||
onRibbonTab,
|
||||
zoomEnabled,
|
||||
onExportPdf,
|
||||
onExportDxf,
|
||||
onExportSchedule,
|
||||
onExportIfc,
|
||||
onExportObj,
|
||||
onExportStl,
|
||||
viewType,
|
||||
fov,
|
||||
onFov,
|
||||
cameraEnabled,
|
||||
resourcesOpen,
|
||||
onToggleResources,
|
||||
onOpenSettings,
|
||||
onImport,
|
||||
onSaveProject,
|
||||
onOpenProject,
|
||||
menuBar,
|
||||
viewControls,
|
||||
}: TopBarProps) {
|
||||
// Dezente Scrollleiste: die Oberleiste scrollt horizontal (overflow-x). Statt
|
||||
// der prägnanten Standard-Leiste blenden wir eine eigene, sehr ruhige Leiste
|
||||
@@ -1911,36 +1460,17 @@ export function TopBar({
|
||||
</button>
|
||||
</div>
|
||||
{aboutOpen && <AboutDialog onClose={() => setAboutOpen(false)} />}
|
||||
<QuickAccess
|
||||
onOpenProject={onOpenProject}
|
||||
onSaveProject={onSaveProject}
|
||||
onImport={onImport}
|
||||
onExportPdf={onExportPdf}
|
||||
onExportDxf={onExportDxf}
|
||||
onExportSchedule={onExportSchedule}
|
||||
onExportIfc={onExportIfc}
|
||||
onExportObj={onExportObj}
|
||||
onExportStl={onExportStl}
|
||||
onToggleResources={onToggleResources}
|
||||
onOpenSettings={onOpenSettings}
|
||||
resourcesOpen={resourcesOpen}
|
||||
exportEnabled={zoomEnabled}
|
||||
viewType={viewType}
|
||||
fov={fov}
|
||||
onFov={onFov}
|
||||
cameraEnabled={cameraEnabled}
|
||||
/>
|
||||
{menuBar}
|
||||
|
||||
<Sep />
|
||||
{/* Ansichts-/Zoom-/Darstellungs-Steuerung direkt in DERSELBEN Zeile
|
||||
(Mockup: EINE schmale Leiste, kein eigenes Band mehr). */}
|
||||
{viewControls && <Sep />}
|
||||
{viewControls}
|
||||
|
||||
{/* Ribbon-Tab-Reiter (2D/3D/BIM/Text/Ansichten) — sitzen in dieser Zeile,
|
||||
der Ribbon-Inhalt folgt darunter (RibbonBar). So gibt es nur ZWEI
|
||||
Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */}
|
||||
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
|
||||
|
||||
{/* Rechte Gruppe: Projektname. Die Fensterknöpfe sitzen ganz links (macOS-
|
||||
Titelleisten-Konvention); Datei-/Export-Aktionen in der Quick-Access-
|
||||
Leiste; die Arbeitsumgebung (Layout) liegt jetzt in den Einstellungen. */}
|
||||
{/* Rechte Gruppe: Datei-/Export-Aktionen (Quick-Access) + Projektname —
|
||||
per margin-left:auto an den rechten Rand geschoben (Mockup: ⤓ ⤒
|
||||
Projektname ganz rechts). Die Fensterknöpfe sitzen ganz links
|
||||
(macOS-Titelleisten-Konvention). */}
|
||||
<div className="topbar-right">
|
||||
<span className="project-name">{project.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,34 @@ export function CommandIcon({ name }: { name: string }) {
|
||||
<rect x="6" y="6.5" width="4" height="3" strokeDasharray="1.4 1.2" />
|
||||
</svg>
|
||||
);
|
||||
case "extrude":
|
||||
// Extrusion: Grundfläche + nach oben herausgezogenes Volumen (Prisma).
|
||||
return (
|
||||
<svg {...common}>
|
||||
<rect x="2.5" y="7.5" width="7" height="5" />
|
||||
<path d="M2.5 7.5 L5.5 4.5 L12.5 4.5 L9.5 7.5" />
|
||||
<path d="M9.5 12.5 L12.5 9.5 L12.5 4.5" />
|
||||
</svg>
|
||||
);
|
||||
case "sectionline":
|
||||
// Schnittlinie: Achse mit Blickrichtungs-Winkeln (SIA-Schnittsymbol).
|
||||
return (
|
||||
<svg {...common}>
|
||||
<line x1="2.5" y1="8" x2="13.5" y2="8" />
|
||||
<polyline points="4.5,4.5 4.5,8 8,8" />
|
||||
<polyline points="11.5,11.5 11.5,8 8,8" />
|
||||
</svg>
|
||||
);
|
||||
case "viewline":
|
||||
// Ansichtslinie: Standpunkt (Kreis) mit Blickkegel.
|
||||
return (
|
||||
<svg {...common}>
|
||||
<circle cx="4" cy="8" r="1.6" />
|
||||
<line x1="5.4" y1="8" x2="13" y2="4" />
|
||||
<line x1="5.4" y1="8" x2="13" y2="12" />
|
||||
<line x1="13" y1="4" x2="13" y2="12" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
// Ribbon-Inhaltsleiste (docs/design/ribbon-ui-plan.md) — rendert den INHALT des
|
||||
// aktiven Ribbon-Tabs. Die Tab-REITER selbst sitzen in der TopBar-Zeile (eine
|
||||
// Leiste: globales Chrome + Tabs), damit es nur ZWEI Bänder gibt: TopBar-Zeile
|
||||
// (mit Tabs) oben, Ribbon-Inhalt darunter. Der aktive Tab wird als `tab`-Prop
|
||||
// hereingereicht (State liegt in App, geteilt mit der TopBar).
|
||||
// Ribbon-Inhaltsleiste (docs/design/ribbon-ui-plan.md) — rendert die Gruppen
|
||||
// EINES festen Tabs (kein Tab-Umschalter mehr, v12: Werkzeuge leben im
|
||||
// ToolsPanel). App rendert mehrere `<RibbonBar tab="...">`-Instanzen permanent
|
||||
// nebeneinander (2d/bim/custom) statt einer einzigen, umschaltbaren — jede
|
||||
// zeigt nur die Befehls-Gruppen ohne Panel-Entsprechung.
|
||||
//
|
||||
// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive, wie
|
||||
// in der Sidebar); Befehls-Elemente über `onRunCommand` (engine.start). Der
|
||||
// „Ansichten"-Tab trägt reichen Custom-Inhalt (`viewsContent`, aus der TopBar
|
||||
// gemergt) statt datengetriebener Item-Gruppen.
|
||||
// im ToolsPanel); Befehls-Elemente über `onRunCommand` (engine.start).
|
||||
//
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { t } from "../../i18n";
|
||||
import { ToolIcon } from "../../panels/ToolsPanel";
|
||||
import { getTool } from "../../tools/tools";
|
||||
@@ -43,13 +40,6 @@ export interface RibbonBarProps {
|
||||
onSelectTool: (id: ToolId) => void;
|
||||
/** Engine-Befehl starten (Ändern-Gruppe u. a.). */
|
||||
onRunCommand: (name: string) => void;
|
||||
/**
|
||||
* Reicher Custom-Inhalt je Tab (statt datengetriebener Item-Gruppen) — z. B.
|
||||
* „Ansichten" (`ViewRibbonTab`) und „Text" (`TextGroup`). Von App geliefert,
|
||||
* damit deren Setter/State dort bleiben. Ein Tab ohne Custom-Inhalt und ohne
|
||||
* Gruppen zeigt den Leer-Hinweis.
|
||||
*/
|
||||
tabContent?: Partial<Record<RibbonTabId, ReactNode>>;
|
||||
/** Persistierte Item-Schlüssel der modularen Custom-Bar (Tab „Eigene"). */
|
||||
customItems: string[];
|
||||
/** Ein Item in der Custom-Bar an-/abwählen (Picker im „Eigene"-Tab). */
|
||||
@@ -106,35 +96,6 @@ function RibbonButton({ item, ctx }: { item: RibbonItem; ctx: ButtonCtx }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Tab-REITER — als eigene Komponente exportiert, weil sie in der TopBar-Zeile
|
||||
* gerendert werden (nicht hier). Rein gesteuert über `tab`/`onTab`.
|
||||
*/
|
||||
export function RibbonTabs({
|
||||
tab,
|
||||
onTab,
|
||||
}: {
|
||||
tab: RibbonTabId;
|
||||
onTab: (id: RibbonTabId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="ribbon-tabs" role="tablist">
|
||||
{RIBBON_TABS.map((tt) => (
|
||||
<button
|
||||
key={tt.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tt.id === tab}
|
||||
className={`ribbon-tab${tt.id === tab ? " active" : ""}`}
|
||||
onClick={() => onTab(tt.id)}
|
||||
>
|
||||
{t(tt.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulare Custom-Bar (Tab „Eigene") — rendert die vom Nutzer gewählten Items
|
||||
* plus einen „+"-Picker, der ALLE verfügbaren Werkzeuge/Befehle mit Häkchen für
|
||||
@@ -184,7 +145,6 @@ function CustomBar({
|
||||
|
||||
export function RibbonBar(props: RibbonBarProps) {
|
||||
const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0];
|
||||
const custom = props.tabContent?.[current.id];
|
||||
const ctx: ButtonCtx = {
|
||||
activeTool: props.activeTool,
|
||||
activeCommand: props.activeCommand,
|
||||
@@ -195,9 +155,8 @@ export function RibbonBar(props: RibbonBarProps) {
|
||||
|
||||
return (
|
||||
<div className="ribbon">
|
||||
{/* Inhalt des aktiven Tabs. Tabs mit reichem Custom-Inhalt („Ansichten",
|
||||
„Text") rendern diesen; der „Eigene"-Tab die modulare Custom-Bar; sonst
|
||||
die datengetriebenen Item-Gruppen. */}
|
||||
{/* Der „Eigene"-Tab rendert die modulare Custom-Bar; sonst die
|
||||
datengetriebenen Item-Gruppen. */}
|
||||
<div className="ribbon-body">
|
||||
{current.id === "custom" ? (
|
||||
<CustomBar
|
||||
@@ -205,8 +164,6 @@ export function RibbonBar(props: RibbonBarProps) {
|
||||
onToggle={props.onToggleCustomItem}
|
||||
ctx={ctx}
|
||||
/>
|
||||
) : custom ? (
|
||||
custom
|
||||
) : current.groups.length === 0 ? (
|
||||
<div className="ribbon-empty">{t("ribbon.empty")}</div>
|
||||
) : (
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface RibbonGroup {
|
||||
}
|
||||
|
||||
/** Kennung eines Ribbon-Tabs (fest, für Tab-State + i18n). */
|
||||
export type RibbonTabId = "views" | "2d" | "3d" | "bim" | "custom";
|
||||
export type RibbonTabId = "2d" | "bim" | "custom";
|
||||
|
||||
/** Ein Ribbon-Tab: Beschriftung + gruppierte Elemente. */
|
||||
export interface RibbonTab {
|
||||
@@ -37,41 +37,24 @@ export interface RibbonTab {
|
||||
groups: RibbonGroup[];
|
||||
}
|
||||
|
||||
const t = (id: ToolId): RibbonItem => ({ kind: "tool", id });
|
||||
const c = (name: string): RibbonItem => ({ kind: "command", name });
|
||||
|
||||
/**
|
||||
* Die Ribbon-Tabs (Reihenfolge Ansichten·2D·3D·BIM). „Ansichten" ist der
|
||||
* Standard-Tab (zuerst, initial aktiv) und trägt reichen Custom-Inhalt: die aus
|
||||
* der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung UND die Text-/Font-
|
||||
* Formatierung auf DERSELBEN Leiste (`ViewRibbonTab` — `groups` bleibt leer). 2D
|
||||
* (Zeichnen + Ändern) und BIM (Bauteile) sind datengetrieben aus Werkzeugen/
|
||||
* Befehlen; 3D ist vorerst leer (spätere Phase).
|
||||
* Die Ribbon-Tabs (2D·BIM·Eigene) — keine Tab-Reiter mehr in der UI (v12,
|
||||
* siehe layout.ts): App rendert alle drei permanent nebeneinander (kein
|
||||
* Umschalter). Reine Werkzeug-Elemente (`t(...)`, persistentes Werkzeug wie
|
||||
* Wand/Linie/Fenster) leben AUSSCHLIESSLICH im ToolsPanel (kein Doppel-
|
||||
* Auftritt). Reine Befehls-Elemente (`c(...)`, Einmal-Aktionen wie
|
||||
* Verschieben/Kopieren/Säule setzen ohne eigenen Werkzeug-Modus) bleiben hier,
|
||||
* da sie im Panel keine Entsprechung haben. Ansichts-/Zoom-/Darstellungs-
|
||||
* Steuerung + Text-Formatierung sitzen direkt in App (ViewRibbonTab), nicht
|
||||
* in dieser Registry.
|
||||
*/
|
||||
export const RIBBON_TABS: RibbonTab[] = [
|
||||
{
|
||||
// Custom-Inhalt (ViewRibbonTab): Ansichts-/Zoom-/Darstellungssteuerung + Text.
|
||||
id: "views",
|
||||
labelKey: "ribbon.tab.views",
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
id: "2d",
|
||||
labelKey: "ribbon.tab.2d",
|
||||
groups: [
|
||||
{
|
||||
titleKey: "ribbon.group.draw",
|
||||
items: [
|
||||
t("select"),
|
||||
t("line"),
|
||||
t("polyline"),
|
||||
t("rect"),
|
||||
t("circle"),
|
||||
t("arc"),
|
||||
t("text"),
|
||||
t("textbox"),
|
||||
],
|
||||
},
|
||||
{
|
||||
titleKey: "ribbon.group.modify",
|
||||
items: [
|
||||
@@ -87,28 +70,16 @@ export const RIBBON_TABS: RibbonTab[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3d",
|
||||
labelKey: "ribbon.tab.3d",
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
id: "bim",
|
||||
labelKey: "ribbon.tab.bim",
|
||||
groups: [
|
||||
{
|
||||
// "extrude" war vormals im (jetzt entfernten) 3D-Ribbon-Tab
|
||||
// (Ribbon3dTab) — einziger Zugang zum Befehl, daher hier statt
|
||||
// gestrichen.
|
||||
titleKey: "ribbon.group.components",
|
||||
items: [
|
||||
t("wall"),
|
||||
t("window"),
|
||||
t("door"),
|
||||
t("stair"),
|
||||
c("column"),
|
||||
t("ceiling"),
|
||||
c("ceilingopening"),
|
||||
c("roof"),
|
||||
t("room"),
|
||||
],
|
||||
items: [c("column"), c("ceilingopening"), c("roof"), c("extrude")],
|
||||
},
|
||||
{
|
||||
titleKey: "ribbon.group.sections",
|
||||
|
||||
Reference in New Issue
Block a user