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:
@@ -13,8 +13,9 @@ import { t } from "../i18n";
|
||||
import { Dropdown } from "../ui/Dropdown";
|
||||
|
||||
/**
|
||||
* Beschriftungs-Keys je Modus (Werte englisch, vgl. types.ts) — exakt die
|
||||
* fünf DOSSIER-Modi aus docs/design/context-menu.md.
|
||||
* Beschriftungs-Keys je Modus (Werte englisch, vgl. types.ts) — die fünf
|
||||
* DOSSIER-Modi aus docs/design/context-menu.md plus die eigene Ergänzung
|
||||
* "locked" (aktiv farbig, andere gesperrt aber NICHT grau).
|
||||
*/
|
||||
const MODE_OPTIONS: { value: DisplayMode; labelKey: string }[] = [
|
||||
{ value: "all_force", labelKey: "display.all_force" },
|
||||
@@ -22,6 +23,7 @@ const MODE_OPTIONS: { value: DisplayMode; labelKey: string }[] = [
|
||||
{ value: "active", labelKey: "display.active" },
|
||||
{ value: "grey", labelKey: "display.grey" },
|
||||
{ value: "grey_locked", labelKey: "display.grey_locked" },
|
||||
{ value: "locked", labelKey: "display.locked" },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ import { itemDisplay } from "./displayMode";
|
||||
import { usePanelHost } from "./host";
|
||||
import type { DrawingLevel } from "./host";
|
||||
import type { DisplayMode } from "./types";
|
||||
import { DisplayModeSelect } from "./DisplayModeSelect";
|
||||
import type { DrawingLevelKind } from "../model/types";
|
||||
import { t } from "../i18n";
|
||||
|
||||
@@ -31,8 +32,9 @@ const KIND_BADGE_KEY: Record<DrawingLevelKind, string> = {
|
||||
drawing: "badge.drawing",
|
||||
};
|
||||
|
||||
/** Kategorien der Zeichnungsebenen, in Anzeigereihenfolge. */
|
||||
const CATEGORY_ORDER: DrawingLevelKind[] = [
|
||||
/** Kategorien der Zeichnungsebenen, in Anzeigereihenfolge. Exportiert für das
|
||||
* native Zeichnungsebenen-Fenster (native/DrawingLevelsWindowApp.tsx). */
|
||||
export const CATEGORY_ORDER: DrawingLevelKind[] = [
|
||||
"floor",
|
||||
"section",
|
||||
"elevation",
|
||||
@@ -40,7 +42,7 @@ const CATEGORY_ORDER: DrawingLevelKind[] = [
|
||||
];
|
||||
|
||||
/** i18n-Key des Kategorie-Titels. */
|
||||
const CATEGORY_TITLE_KEY: Record<DrawingLevelKind, string> = {
|
||||
export const CATEGORY_TITLE_KEY: Record<DrawingLevelKind, string> = {
|
||||
floor: "levelCat.floor",
|
||||
section: "levelCat.section",
|
||||
elevation: "levelCat.elevation",
|
||||
@@ -48,7 +50,7 @@ const CATEGORY_TITLE_KEY: Record<DrawingLevelKind, string> = {
|
||||
};
|
||||
|
||||
/** i18n-Key der Beschriftung des „+"-Glyphs (Tooltip/aria-label). */
|
||||
const CATEGORY_ADD_KEY: Record<DrawingLevelKind, string> = {
|
||||
export const CATEGORY_ADD_KEY: Record<DrawingLevelKind, string> = {
|
||||
floor: "levels.addFloor",
|
||||
section: "levels.addSection",
|
||||
elevation: "levels.addElevation",
|
||||
@@ -93,10 +95,13 @@ export function DrawingLevelsPanel() {
|
||||
|
||||
return (
|
||||
<section className="nav-group">
|
||||
<header className="nav-group-head">
|
||||
<span className="nav-group-title">{t("nav.drawingLevels")}</span>
|
||||
</header>
|
||||
|
||||
{/* Kein eigener Titel/Kopf mehr — der PanelFrame liefert die EINE
|
||||
Kopfzeile; die Fenster-Ausklinken-Aktion sitzt dort via headerActions
|
||||
(DrawingLevelsHeaderActions), kein doppelter Titel. */}
|
||||
<div className="panel-vis">
|
||||
<span className="panel-vis-label">{t("nav.visibility")}</span>
|
||||
<DisplayModeSelect value={displayMode.mode} onChange={displayMode.setMode} />
|
||||
</div>
|
||||
{CATEGORY_ORDER.map((kind) => {
|
||||
const levelsOfKind = levelsTopDown.filter((l) => l.kind === kind);
|
||||
const isOpen = !collapsed[kind];
|
||||
@@ -151,8 +156,28 @@ export function DrawingLevelsPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Eine Zeile im Zeichnungsebenen-Bereich. */
|
||||
function LevelRow({
|
||||
/** Aktionen für die gemeinsame PanelFrame-Kopfzeile (statt eigener Titelzeile):
|
||||
* Zahnrad → Einstellungen (Darstellungsfarben/Projekt-Felder). */
|
||||
export function DrawingLevelsHeaderActions() {
|
||||
const host = usePanelHost();
|
||||
return (
|
||||
<button
|
||||
className="nav-settings"
|
||||
aria-label={t("topbar.settings")}
|
||||
title={t("topbar.settings")}
|
||||
onClick={host.onOpenSettings}
|
||||
>
|
||||
<span className="material-symbols-outlined" aria-hidden="true">
|
||||
settings
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** Eine Zeile im Zeichnungsebenen-Bereich. Exportiert, damit das native
|
||||
* Zeichnungsebenen-Fenster (native/DrawingLevelsWindowApp.tsx) dieselbe
|
||||
* Zeilen-Darstellung wiederverwendet statt sie zu duplizieren. */
|
||||
export function LevelRow({
|
||||
level,
|
||||
active,
|
||||
mode,
|
||||
|
||||
@@ -26,6 +26,8 @@ import { t } from "../i18n";
|
||||
const CLASS_ORDER: Exclude<ScheduleKind, "Tür">[] = [
|
||||
"Wand",
|
||||
"Decke",
|
||||
"Dach",
|
||||
"Stütze",
|
||||
"Fenster",
|
||||
"Öffnung",
|
||||
"Treppe",
|
||||
@@ -36,6 +38,8 @@ const CLASS_ORDER: Exclude<ScheduleKind, "Tür">[] = [
|
||||
const CLASS_LABEL_KEY: Record<Exclude<ScheduleKind, "Tür">, string> = {
|
||||
Wand: "elements.kind.wall",
|
||||
Decke: "elements.kind.ceiling",
|
||||
Dach: "elements.kind.roof",
|
||||
Stütze: "elements.kind.column",
|
||||
Fenster: "elements.kind.window",
|
||||
Öffnung: "elements.kind.opening",
|
||||
Treppe: "elements.kind.stair",
|
||||
|
||||
+64
-15
@@ -4,14 +4,18 @@
|
||||
// Navigator: Farb-Swatch, Code, Name, EyeIcon zum Schalten, eingerückte
|
||||
// Kinder, Kopfzeile mit + Ebene. Daten/Handler über usePanelHost.
|
||||
//
|
||||
// Das Panel deklariert hasDisplayMode. Kategorien kennen keine „aktive"
|
||||
// Auswahl (anders als Zeichnungsebenen), daher ist isActive je Zeile false;
|
||||
// `itemDisplay` ergibt damit (fünf DOSSIER-Modi):
|
||||
// Das Panel deklariert hasDisplayMode. „Aktiv" ist die per Klick gewählte
|
||||
// Kategorie (activeCategoryCode, Zeichen-Ziel für neue Elemente); dieselbe
|
||||
// activeCategoryCode speist auch App.tsx' categoryDisplay-Resolver, der die
|
||||
// tatsächliche Grundriss-/3D-Darstellung steuert. `itemDisplay` ergibt damit
|
||||
// (fünf DOSSIER-Modi plus eigene Ergänzung "locked"):
|
||||
// • "all_force" — alle erzwungen sichtbar; Augen gedimmt.
|
||||
// • "all" — alle sichtbaren normal (Standard, wie bisher).
|
||||
// • "active" — keine Kategorie aktiv → alle gedimmt.
|
||||
// • "grey" — sichtbare Kategorien ausgegraut.
|
||||
// • "grey_locked" — wie „grey", zusätzlich gesperrt.
|
||||
// • "active" — nur die gewählte Kategorie sichtbar, Rest ausgeblendet.
|
||||
// • "grey" — gewählte normal, übrige sichtbare ausgegraut.
|
||||
// • "grey_locked" — wie „grey", übrige zusätzlich gesperrt.
|
||||
// • "locked" — gewählte normal, übrige sichtbare UNVERÄNDERT farbig,
|
||||
// nur gesperrt (nicht anwählbar/bearbeitbar).
|
||||
// Wie im Zeichnungsebenen-Panel bleibt jede Zeile bedienbar (die Liste ist die
|
||||
// Steuerung); statt auszublenden dimmen wir gedimmte Zeilen per Inline-Opacity
|
||||
// (styles.css bleibt unangetastet).
|
||||
@@ -23,6 +27,7 @@ import { itemDisplay } from "./displayMode";
|
||||
import { usePanelHost } from "./host";
|
||||
import type { LayerCategory } from "./host";
|
||||
import type { DisplayMode } from "./types";
|
||||
import { DisplayModeSelect } from "./DisplayModeSelect";
|
||||
import { t } from "../i18n";
|
||||
|
||||
/** Inhalt des Panels „Ebenen". */
|
||||
@@ -46,15 +51,13 @@ export function LayersPanel() {
|
||||
|
||||
return (
|
||||
<section className="nav-group">
|
||||
<header className="nav-group-head">
|
||||
<span className="nav-group-title">{t("nav.layers")}</span>
|
||||
<span className="nav-group-actions">
|
||||
<button className="nav-add" onClick={host.onAddCategory}>
|
||||
{t("layers.addCategory")}
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{/* Kein eigener Titel/Kopf mehr — der PanelFrame liefert die EINE
|
||||
Kopfzeile; die „+ Kategorie"-Aktion sitzt dort via headerActions
|
||||
(LayersHeaderActions), kein doppelter „Ebenen"-Titel. */}
|
||||
<div className="panel-vis">
|
||||
<span className="panel-vis-label">{t("nav.visibility")}</span>
|
||||
<DisplayModeSelect value={displayMode.mode} onChange={displayMode.setMode} />
|
||||
</div>
|
||||
<div className="nav-list">
|
||||
{project.layers.map((cat) => (
|
||||
<CategoryRow
|
||||
@@ -66,6 +69,7 @@ export function LayersPanel() {
|
||||
onSelect={host.onSelectCategory}
|
||||
onToggle={toggle}
|
||||
onContext={onRowContext}
|
||||
onSettings={host.onOpenLayerSettings}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -82,6 +86,7 @@ function CategoryRow({
|
||||
onSelect,
|
||||
onToggle,
|
||||
onContext,
|
||||
onSettings,
|
||||
}: {
|
||||
category: LayerCategory;
|
||||
depth: number;
|
||||
@@ -90,6 +95,7 @@ function CategoryRow({
|
||||
onSelect: (code: string) => void;
|
||||
onToggle: (code: string) => void;
|
||||
onContext: (code: string, e: React.MouseEvent) => void;
|
||||
onSettings: (code: string, clientX: number, clientY: number) => void;
|
||||
}) {
|
||||
// „aktiv" = diese Kategorie ist das Zeichen-Ziel (per Klick gesetzt).
|
||||
const isActive = category.code === activeCode;
|
||||
@@ -137,6 +143,19 @@ function CategoryRow({
|
||||
<span className="cat-code">{category.code}</span>
|
||||
<span className="cat-swatch" style={{ background: category.color }} />
|
||||
<span className="cat-name">{category.name}</span>
|
||||
<button
|
||||
className="cat-settings"
|
||||
aria-label={t("nav.categorySettings", { name: category.name })}
|
||||
title={t("nav.categorySettings", { name: category.name })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSettings(category.code, e.clientX, e.clientY);
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined" aria-hidden="true">
|
||||
settings
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{category.children?.map((child) => (
|
||||
<CategoryRow
|
||||
@@ -148,8 +167,38 @@ function CategoryRow({
|
||||
onSelect={onSelect}
|
||||
onToggle={onToggle}
|
||||
onContext={onContext}
|
||||
onSettings={onSettings}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Aktionen für die gemeinsame PanelFrame-Kopfzeile (statt eigener Titelzeile):
|
||||
* „+ Kategorie" + Zahnrad → Einstellungen (wie im Zeichnungsebenen-Kopf).
|
||||
* Läuft im PanelHostContext, nutzt daher usePanelHost. */
|
||||
export function LayersHeaderActions() {
|
||||
const host = usePanelHost();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="nav-settings"
|
||||
onClick={host.onOpenSettings}
|
||||
aria-label={t("topbar.settings")}
|
||||
title={t("topbar.settings")}
|
||||
>
|
||||
<span className="material-symbols-outlined" aria-hidden="true">
|
||||
settings
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="nav-add"
|
||||
onClick={host.onAddCategory}
|
||||
aria-label={t("layers.addCategory")}
|
||||
title={t("layers.addCategory")}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,6 +242,35 @@ export function ObjectInfoPanel() {
|
||||
onCommit={(v) => host.onSetDrawingCircleRadius(v)}
|
||||
/>
|
||||
)}
|
||||
{/* Kontext-Objekt (importiertes Gebäude/Terrain/Konturen): reale LV95-
|
||||
Koordinate des Bbox-Mittelpunkts — direkt sichtbar, ob/wohin
|
||||
georeferenziert importiert wurde (Nutzer-Wunsch zum Prüfen). */}
|
||||
{sel.contextObject && (
|
||||
<>
|
||||
<div className="objinfo-sep" />
|
||||
<div className="objinfo-section-label">{t("objinfo.contextObject.section")}</div>
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.contextObject.type")}</span>
|
||||
<span className="objinfo-fval">{t(`objinfo.contextObject.type.${sel.contextObject.objType}`)}</span>
|
||||
</div>
|
||||
{sel.contextObject.lv95Center ? (
|
||||
<>
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.contextObject.lv95E")}</span>
|
||||
<span className="objinfo-fval">{sel.contextObject.lv95Center.e.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.contextObject.lv95N")}</span>
|
||||
<span className="objinfo-fval">{sel.contextObject.lv95Center.n.toFixed(1)}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-fval objinfo-muted">{t("objinfo.contextObject.noAnchor")}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Die element-spezifischen Abschnitte (Wand/Decke/Öffnung/Treppe/Raum)
|
||||
werden jetzt im Attribute-Panel gerendert (AttributesPanel importiert
|
||||
die exportierten *Section-Komponenten). */}
|
||||
@@ -393,6 +422,20 @@ export function SectionLineSection({
|
||||
{t("objinfo.sectionLine.flip")}
|
||||
</button>
|
||||
|
||||
{/* 3D-Live-Schnitt an diese Linie binden (nur mit gesetzter Linie sinnvoll):
|
||||
der Viewport clippt dann vertikal entlang dieser Ebene. */}
|
||||
{line && (
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.sectionLine.cut3d")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={host.section3dCutActive}
|
||||
onChange={() => host.onToggleSection3dCut(level.id)}
|
||||
title={t("objinfo.sectionLine.cut3d")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Endpunkt-Koordinaten (nur wenn eine Linie gesetzt ist). */}
|
||||
{line && (
|
||||
<>
|
||||
@@ -854,6 +897,27 @@ export function StairSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pfeil-Darstellung (offener Winkel- oder gefüllter SIA-Vollpfeil). */}
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.stair.arrow")}</span>
|
||||
<div className="objinfo-segment">
|
||||
<button
|
||||
type="button"
|
||||
className={"objinfo-seg-btn" + ((stair.arrowStyle ?? "line") === "line" ? " active" : "")}
|
||||
onClick={() => host.onSetStairArrowStyle("line")}
|
||||
>
|
||||
{t("objinfo.stair.arrow.line")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={"objinfo-seg-btn" + (stair.arrowStyle === "filled" ? " active" : "")}
|
||||
onClick={() => host.onSetStairArrowStyle("filled")}
|
||||
>
|
||||
{t("objinfo.stair.arrow.filled")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editierbare Maße. */}
|
||||
<DimensionField
|
||||
label={t("objinfo.stair.width")}
|
||||
|
||||
@@ -54,12 +54,15 @@ export function PanelFrame({
|
||||
<section className="panel-frame">
|
||||
<header className="panel-head">
|
||||
<span className="panel-title">{t(def.title)}</span>
|
||||
{showMode && (
|
||||
<DisplayModeSelect
|
||||
value={displayMode as DisplayMode}
|
||||
onChange={onDisplayModeChange as (m: DisplayMode) => void}
|
||||
/>
|
||||
)}
|
||||
<span className="panel-head-actions">
|
||||
{def.headerActions}
|
||||
{showMode && (
|
||||
<DisplayModeSelect
|
||||
value={displayMode as DisplayMode}
|
||||
onChange={onDisplayModeChange as (m: DisplayMode) => void}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</header>
|
||||
<div className="panel-body">{def.render(ctx ?? {})}</div>
|
||||
</section>
|
||||
|
||||
+82
-23
@@ -19,6 +19,8 @@ import type { ContextObject } from "../model/types";
|
||||
import { ContextImportDialog } from "../ui/ContextImportDialog";
|
||||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { useStore } from "../state/appStore";
|
||||
import { isTauriRuntime } from "../io/projectFile";
|
||||
import { openContextImportWindowNative } from "../native/contextImportWindow";
|
||||
|
||||
/** Typ-spezifisches Badge-Label (i18n-Key) je Kontext-Objekt-Art. */
|
||||
function typeBadgeKey(obj: ContextObject): TranslationKey {
|
||||
@@ -29,9 +31,43 @@ function typeBadgeKey(obj: ContextObject): TranslationKey {
|
||||
return "site.type.contours";
|
||||
case "terrainMesh":
|
||||
return "site.type.terrain";
|
||||
case "image":
|
||||
return "site.type.image";
|
||||
}
|
||||
}
|
||||
|
||||
/** Ebenen-/Gruppenname eines Kontext-Objekts (aus `layer`, sonst Typ-Fallback). */
|
||||
function layerOf(obj: ContextObject): string {
|
||||
if (obj.layer && obj.layer.trim() !== "") return obj.layer;
|
||||
switch (obj.type) {
|
||||
case "importedMesh":
|
||||
return t("site.group.imported");
|
||||
case "terrainMesh":
|
||||
return t("site.group.terrain");
|
||||
case "contourSet":
|
||||
return t("site.group.contours");
|
||||
case "image":
|
||||
return t("site.group.image");
|
||||
}
|
||||
}
|
||||
|
||||
/** Gruppiert Kontext-Objekte nach Ebene, Reihenfolge des ersten Auftretens. */
|
||||
function groupByLayer(objects: ContextObject[]): { layer: string; items: ContextObject[] }[] {
|
||||
const order: string[] = [];
|
||||
const byLayer = new Map<string, ContextObject[]>();
|
||||
for (const obj of objects) {
|
||||
const layer = layerOf(obj);
|
||||
let items = byLayer.get(layer);
|
||||
if (!items) {
|
||||
items = [];
|
||||
byLayer.set(layer, items);
|
||||
order.push(layer);
|
||||
}
|
||||
items.push(obj);
|
||||
}
|
||||
return order.map((layer) => ({ layer, items: byLayer.get(layer)! }));
|
||||
}
|
||||
|
||||
export function SitePanel() {
|
||||
const host = usePanelHost();
|
||||
const notify = useStore((s) => s.notify);
|
||||
@@ -72,7 +108,10 @@ export function SitePanel() {
|
||||
<button
|
||||
type="button"
|
||||
className="site-btn"
|
||||
onClick={() => setCtxImportOpen(true)}
|
||||
onClick={() => {
|
||||
if (isTauriRuntime()) void openContextImportWindowNative();
|
||||
else setCtxImportOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("site.importLocation")}
|
||||
</button>
|
||||
@@ -117,37 +156,57 @@ export function SitePanel() {
|
||||
|
||||
<div className="site-sep" />
|
||||
|
||||
{/* Liste der Kontext-Objekte (Name + Typ-Badge + Entfernen). */}
|
||||
{/* Kontext-Objekte nach Ebene (`layer`) gruppiert: importierte Gebäude
|
||||
liegen als EINZELNE, getrennt lösch-/anwählbare Elemente unter ihrer
|
||||
Ebene (z. B. „Gebäude"), Terrain unter „Terrain". Je Gruppe ein Kopf
|
||||
mit Anzahl + „ganze Ebene löschen". */}
|
||||
{objects.length === 0 ? (
|
||||
<div className="site-empty">{t("site.empty")}</div>
|
||||
) : (
|
||||
<ul className="site-list">
|
||||
{objects.map((obj) => (
|
||||
<li
|
||||
key={obj.id}
|
||||
className={
|
||||
"site-row" +
|
||||
(host.selectedContextObjectIds.includes(obj.id) ? " site-row-selected" : "")
|
||||
}
|
||||
>
|
||||
<span className="site-name" title={obj.name}>
|
||||
{obj.name}
|
||||
</span>
|
||||
<span className={"site-badge site-badge-" + obj.type}>
|
||||
{t(typeBadgeKey(obj))}
|
||||
</span>
|
||||
groupByLayer(objects).map(({ layer, items }) => (
|
||||
<div key={layer} className="site-group">
|
||||
<div className="site-group-head">
|
||||
<span className="site-group-name">{layer}</span>
|
||||
<span className="site-group-count">{items.length}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="site-remove"
|
||||
title={t("site.delete")}
|
||||
aria-label={t("site.delete")}
|
||||
onClick={() => host.onRemoveContextObject(obj.id)}
|
||||
title={t("site.deleteLayer")}
|
||||
aria-label={t("site.deleteLayer")}
|
||||
onClick={() => host.onRemoveContextObjects(items.map((o) => o.id))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="site-list">
|
||||
{items.map((obj) => (
|
||||
<li
|
||||
key={obj.id}
|
||||
className={
|
||||
"site-row" +
|
||||
(host.selectedContextObjectIds.includes(obj.id) ? " site-row-selected" : "")
|
||||
}
|
||||
>
|
||||
<span className="site-name" title={obj.name}>
|
||||
{obj.name}
|
||||
</span>
|
||||
<span className={"site-badge site-badge-" + obj.type}>
|
||||
{t(typeBadgeKey(obj))}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="site-remove"
|
||||
title={t("site.delete")}
|
||||
aria-label={t("site.delete")}
|
||||
onClick={() => host.onRemoveContextObject(obj.id)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<div className="site-hint">{t("site.dwgHint")}</div>
|
||||
|
||||
+153
-52
@@ -1,18 +1,23 @@
|
||||
// Inhalts-Panel „Werkzeuge" — die vertikale Werkzeug-Palette (ArchiCAD-/
|
||||
// Vectorworks-Stil): eine Zeile je Zeichenwerkzeug, jeweils Icon + Name, das
|
||||
// aktive Werkzeug hervorgehoben. Darunter — wenn das Wand-Werkzeug aktiv ist —
|
||||
// ein Wandtyp-Auswahlfeld, und ein kompakter Fang-Abschnitt mit Checkboxen.
|
||||
// Inhalts-Panel „Werkzeuge" — die Werkzeug-Palette (ArchiCAD-/Vectorworks-Stil).
|
||||
// Oben ein Umschalter „Liste ↔ Symbole" (Icon+Name als Zeilen ODER kompaktes
|
||||
// Icon-Raster). Darunter drei einheitlich gestylte Rubriken: 2D (Zeichnen),
|
||||
// 3D (BIM-Bauteile) und Bearbeiten (Ändern-Aktionen) — Werkzeuge UND Befehle
|
||||
// teilen denselben Knopf-Look (`.tp-item`), damit „Ändern" nicht mehr optisch
|
||||
// aus der Reihe fällt.
|
||||
//
|
||||
// Daten/Handler kommen über usePanelHost (kein Prop-Drilling). Die Werkzeug-
|
||||
// Liste und ihre Metadaten stammen aus der Tools-Registry (TOOL_ORDER/getTool).
|
||||
// Daten/Handler kommen über usePanelHost (kein Prop-Drilling). Werkzeug-Metadaten
|
||||
// aus der Tools-Registry (getTool), Befehls-Labels aus der Command-Registry.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). Alle sichtbaren
|
||||
// Texte über t(...).
|
||||
|
||||
import { useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { usePanelHost } from "./host";
|
||||
import type { ToolId } from "./host";
|
||||
import { TOOL_ORDER, getTool } from "../tools/tools";
|
||||
import { getTool } from "../tools/tools";
|
||||
import { getCommand } from "../commands/registry";
|
||||
import { CommandIcon } from "../ui/ribbon/CommandIcon";
|
||||
|
||||
// ── Icons ──────────────────────────────────────────────────────────────────
|
||||
// Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug
|
||||
@@ -31,18 +36,17 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
};
|
||||
switch (id) {
|
||||
case "select":
|
||||
// Pfeil-Cursor.
|
||||
// Pfeil-Cursor (klassischer Zeiger mit Schweif).
|
||||
return (
|
||||
<svg {...common} fill="currentColor" stroke="none">
|
||||
<path d="M3 2l9 5-3.6 1.1L10.4 13 8.5 13.7 6.6 9.4 3 11z" />
|
||||
<svg {...common} fill="currentColor" stroke="currentColor" strokeWidth={0.8}>
|
||||
<path d="M4 2.5 L4 13.2 L6.9 10.4 L8.9 14.6 L10.7 13.8 L8.7 9.7 L12.6 9.6 Z" />
|
||||
</svg>
|
||||
);
|
||||
case "wall":
|
||||
// Zwei parallele Linien (Wandband).
|
||||
// Wandband: zwei parallele Kanten mit geschlossenen Enden (Wandstück).
|
||||
return (
|
||||
<svg {...common}>
|
||||
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
||||
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
||||
<rect x="2" y="5.5" width="12" height="5" />
|
||||
</svg>
|
||||
);
|
||||
case "ceiling":
|
||||
@@ -55,29 +59,37 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
</svg>
|
||||
);
|
||||
case "window":
|
||||
// Fenster: Rahmen-Rechteck mit Mittelsprosse (Verglasung).
|
||||
// Fenster (SIA-Aufsicht): Wandöffnung mit doppelter Glaslinie.
|
||||
return (
|
||||
<svg {...common}>
|
||||
<rect x="2.5" y="4" width="11" height="8" />
|
||||
<line x1="8" y1="4" x2="8" y2="12" />
|
||||
<line x1="2" y1="5.5" x2="14" y2="5.5" />
|
||||
<line x1="2" y1="10.5" x2="14" y2="10.5" />
|
||||
<line x1="2" y1="8" x2="14" y2="8" />
|
||||
<line x1="2" y1="5.5" x2="2" y2="10.5" />
|
||||
<line x1="14" y1="5.5" x2="14" y2="10.5" />
|
||||
</svg>
|
||||
);
|
||||
case "door":
|
||||
// Tür: Pfosten + Türblatt mit Schwenkbogen.
|
||||
// Tür (SIA-Aufsicht): Türblatt aufrecht + Viertelkreis-Schwenkbogen.
|
||||
return (
|
||||
<svg {...common}>
|
||||
<line x1="3" y1="13" x2="3" y2="4" />
|
||||
<line x1="3" y1="4" x2="11" y2="4" />
|
||||
<path d="M3 13 A9 9 0 0 0 11 4" />
|
||||
<line x1="3.5" y1="13" x2="3.5" y2="3" />
|
||||
<path d="M3.5 3 A9.5 9.5 0 0 1 13 12.5" strokeDasharray="1.4 1.4" />
|
||||
<line x1="3.5" y1="13" x2="13" y2="13" />
|
||||
</svg>
|
||||
);
|
||||
case "stair":
|
||||
// Treppensymbol: gestufte Linie (Tritte) + Lauflinie mit Pfeil.
|
||||
// Treppe (SIA-Aufsicht): Wangen, gleichmäßige Tritte, Lauflinie mit
|
||||
// Aufwärts-Pfeil (Idee: Vollbogen ersetzt durch eine echte Treppe).
|
||||
return (
|
||||
<svg {...common}>
|
||||
<polyline points="2,14 2,11 6,11 6,8 10,8 10,5 14,5 14,2" />
|
||||
<line x1="4" y1="12.5" x2="12" y2="3.5" />
|
||||
<polyline points="10.5,3 12,3.5 11.5,5" />
|
||||
<rect x="3" y="2" width="10" height="12" />
|
||||
<line x1="3" y1="4.4" x2="13" y2="4.4" />
|
||||
<line x1="3" y1="6.8" x2="13" y2="6.8" />
|
||||
<line x1="3" y1="9.2" x2="13" y2="9.2" />
|
||||
<line x1="3" y1="11.6" x2="13" y2="11.6" />
|
||||
<line x1="8" y1="13.2" x2="8" y2="3.4" />
|
||||
<polyline points="6.3,5.1 8,3.2 9.7,5.1" />
|
||||
</svg>
|
||||
);
|
||||
case "room":
|
||||
@@ -118,10 +130,12 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
</svg>
|
||||
);
|
||||
case "arc":
|
||||
// Offener Halbbogen.
|
||||
// Offener Bogen (gut 180°) mit Endpunkt-Markierungen.
|
||||
return (
|
||||
<svg {...common}>
|
||||
<path d="M 2.5 12 A 5.5 5.5 0 0 1 13.5 12" fill="none" />
|
||||
<path d="M 2.5 11 A 5.6 5.6 0 0 1 13.5 11" fill="none" />
|
||||
<circle cx="2.5" cy="11" r="0.9" fill="currentColor" stroke="none" />
|
||||
<circle cx="13.5" cy="11" r="0.9" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
case "text":
|
||||
@@ -147,44 +161,131 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kategorien ───────────────────────────────────────────────────────────────
|
||||
// Werkzeuge/Befehle in drei Rubriken sortiert (Nutzerwunsch: „sortiere nach 2D
|
||||
// und 3D und Bearbeiten"). Befehlsnamen spiegeln die Ribbon-Registry
|
||||
// (ui/ribbon/ribbonItems.ts) — dort liegt die kanonische Liste; hier nur die
|
||||
// Panel-Gruppierung (2D-Zeichnen · 3D/BIM-Bauteile · Bearbeiten-Aktionen).
|
||||
const TOOLS_2D: ToolId[] = ["line", "polyline", "rect", "circle", "arc", "text", "textbox"];
|
||||
const TOOLS_3D: ToolId[] = ["wall", "ceiling", "window", "door", "stair", "room"];
|
||||
const COMMANDS_3D = ["column", "ceilingopening", "roof", "extrude", "sectionline", "viewline"];
|
||||
const COMMANDS_EDIT = ["move", "copy", "rotate", "mirror", "offset", "trim", "join", "measure"];
|
||||
|
||||
type ToolsView = "list" | "icons";
|
||||
const VIEW_STORAGE_KEY = "dossier.toolsPanel.view";
|
||||
|
||||
function loadView(): ToolsView {
|
||||
try {
|
||||
return localStorage.getItem(VIEW_STORAGE_KEY) === "icons" ? "icons" : "list";
|
||||
} catch {
|
||||
return "list";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function ToolsPanel() {
|
||||
const host = usePanelHost();
|
||||
const [view, setView] = useState<ToolsView>(loadView);
|
||||
|
||||
const chooseView = (v: ToolsView) => {
|
||||
setView(v);
|
||||
try {
|
||||
localStorage.setItem(VIEW_STORAGE_KEY, v);
|
||||
} catch {
|
||||
/* Persistenz ist Best-Effort (privater Modus o. ä.). */
|
||||
}
|
||||
};
|
||||
|
||||
// Ein Werkzeug-Knopf (Icon + Name). Boden-gebundene (`floorOnly`) Werkzeuge
|
||||
// sind ohne aktives Geschoss gesperrt; freie 2D-Werkzeuge bleiben nutzbar.
|
||||
const toolButton = (id: ToolId) => {
|
||||
const tool = getTool(id);
|
||||
const active = host.activeTool === id;
|
||||
const disabled = !!tool.floorOnly && !host.toolsEnabled;
|
||||
const label = t(tool.labelKey);
|
||||
const title = disabled ? t("tool.floorOnlyDisabled") : t(tool.hintKey(tool.init()));
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={"tp-item" + (active ? " active" : "") + (disabled ? " disabled" : "")}
|
||||
title={view === "icons" ? label : title}
|
||||
disabled={disabled}
|
||||
onClick={() => host.onSelectTool(id)}
|
||||
>
|
||||
<ToolIcon id={id} />
|
||||
<span className="tp-label">{label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Ein Befehls-Knopf (Ändern-/BIM-Kommando ohne eigenen Werkzeug-Modus) —
|
||||
// gleicher Look wie die Werkzeuge (Nutzerwunsch: nicht „ganz anders").
|
||||
const commandButton = (name: string) => {
|
||||
const cmd = getCommand(name);
|
||||
const label = cmd ? t(cmd.labelKey) : t(`cmd.${name}.label`);
|
||||
const active = host.activeCommand === name;
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className={"tp-item" + (active ? " active" : "")}
|
||||
title={label}
|
||||
onClick={() => host.onRunCommand(name)}
|
||||
>
|
||||
<CommandIcon name={name} />
|
||||
<span className="tp-label">{label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const bodyClass = "tp-body" + (view === "icons" ? " icons" : "");
|
||||
|
||||
return (
|
||||
<div className="tools-panel">
|
||||
{/* Werkzeug-Zeilen: Icon + Name, aktives hervorgehoben. */}
|
||||
{TOOL_ORDER.map((id) => {
|
||||
const tool = getTool(id);
|
||||
const active = host.activeTool === id;
|
||||
// Nur Boden-gebundene (`floorOnly`) Werkzeuge sind ohne aktives Geschoss
|
||||
// gesperrt; freie 2D-Werkzeuge (Linie/Kreis/Text/…) bleiben auf
|
||||
// Zeichnungsebenen nutzbar — Parität mit dem Ribbon (RibbonBar).
|
||||
const disabled = !!tool.floorOnly && !host.toolsEnabled;
|
||||
const title = disabled
|
||||
? t("tool.floorOnlyDisabled")
|
||||
: t(tool.hintKey(tool.init()));
|
||||
const cls =
|
||||
"tool-row" + (active ? " active" : "") + (disabled ? " disabled" : "");
|
||||
return (
|
||||
{/* Anzeige-Umschalter: nur Symbole ODER Symbol + Name (Liste). */}
|
||||
<div className="tp-head">
|
||||
<div className="tp-viewtoggle" role="group" aria-label={t("tools.view.label")}>
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={cls}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={() => host.onSelectTool(id)}
|
||||
className={"tp-viewbtn" + (view === "list" ? " active" : "")}
|
||||
aria-pressed={view === "list"}
|
||||
onClick={() => chooseView("list")}
|
||||
>
|
||||
<ToolIcon id={id} />
|
||||
<span>{t(tool.labelKey)}</span>
|
||||
{t("tools.view.list")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className={"tp-viewbtn" + (view === "icons" ? " active" : "")}
|
||||
aria-pressed={view === "icons"}
|
||||
onClick={() => chooseView("icons")}
|
||||
>
|
||||
{t("tools.view.icons")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wandtyp-/Deckentyp-Auswahl ist ins Attribute-Panel gewandert (Ribbon-UI
|
||||
Phase 3: Werkzeug-Sidebar nicht mehr im Default-Layout). Der Picker
|
||||
erscheint dort bei aktivem Wand-/Decken-Werkzeug. */}
|
||||
{/* Auswahl-Werkzeug: immer als volle Zeile zuoberst (meistgenutzt). */}
|
||||
<div className="tp-select">{toolButton("select")}</div>
|
||||
|
||||
<section className="tp-section">
|
||||
<div className="tp-section-title">{t("tools.cat.2d")}</div>
|
||||
<div className={bodyClass}>{TOOLS_2D.map(toolButton)}</div>
|
||||
</section>
|
||||
|
||||
<section className="tp-section">
|
||||
<div className="tp-section-title">{t("tools.cat.3d")}</div>
|
||||
<div className={bodyClass}>
|
||||
{TOOLS_3D.map(toolButton)}
|
||||
{COMMANDS_3D.map(commandButton)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="tp-section">
|
||||
<div className="tp-section-title">{t("tools.cat.edit")}</div>
|
||||
<div className={bodyClass}>{COMMANDS_EDIT.map(commandButton)}</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { registerPanel } from "./registry";
|
||||
import { DrawingLevelsPanel } from "./DrawingLevelsPanel";
|
||||
import { LayersPanel } from "./LayersPanel";
|
||||
import { DrawingLevelsPanel, DrawingLevelsHeaderActions } from "./DrawingLevelsPanel";
|
||||
import { LayersPanel, LayersHeaderActions } from "./LayersPanel";
|
||||
import { ToolsPanel } from "./ToolsPanel";
|
||||
import { AttributesPanel } from "./AttributesPanel";
|
||||
import { ObjectInfoPanel } from "./ObjectInfoPanel";
|
||||
@@ -82,7 +82,10 @@ registerPanel({
|
||||
// damit der Titel der gewählten Sprache folgt.
|
||||
title: "nav.drawingLevels",
|
||||
icon: <DrawingLevelsTabIcon />,
|
||||
hasDisplayMode: true,
|
||||
// Der Sichtbarkeits-Umschalter sitzt jetzt IM Körper mit „Sichtbarkeit"-
|
||||
// Beschriftung (nicht mehr im PanelFrame-Kopf) — hasDisplayMode: false.
|
||||
hasDisplayMode: false,
|
||||
headerActions: <DrawingLevelsHeaderActions />,
|
||||
render: () => <DrawingLevelsPanel />,
|
||||
});
|
||||
|
||||
@@ -90,7 +93,8 @@ registerPanel({
|
||||
id: BUILTIN_PANEL_IDS.layers,
|
||||
title: "nav.layers",
|
||||
icon: <LayersTabIcon />,
|
||||
hasDisplayMode: true,
|
||||
hasDisplayMode: false,
|
||||
headerActions: <LayersHeaderActions />,
|
||||
render: () => <LayersPanel />,
|
||||
});
|
||||
|
||||
|
||||
@@ -32,8 +32,10 @@ export interface ItemDisplay {
|
||||
* ausgegraut.
|
||||
* • "grey_locked" — wie „grey", übrige (sichtbare, nicht aktive) zusätzlich
|
||||
* gesperrt.
|
||||
* • "locked" — aktives normal; übrige sichtbare UNVERÄNDERT farbig,
|
||||
* aber gesperrt (nicht anwählbar/bearbeitbar).
|
||||
*
|
||||
* Das aktive Element wird in "active"/"grey"/"grey_locked" stets gezeichnet,
|
||||
* Das aktive Element wird in "active"/"grey"/"grey_locked"/"locked" stets gezeichnet,
|
||||
* auch wenn seine Sichtbarkeit abgeschaltet ist — es ist der Arbeitsfokus. Ein
|
||||
* nicht aktives, unsichtbares Element wird (außer in "all_force") nie
|
||||
* gezeichnet.
|
||||
@@ -65,5 +67,12 @@ export function itemDisplay(
|
||||
const other = isVisible && !isActive;
|
||||
return { render: isActive || isVisible, greyed: other, locked: other };
|
||||
}
|
||||
|
||||
case "locked": {
|
||||
// Wie „grey_locked", aber OHNE das Ausgrauen — übrige sichtbare bleiben
|
||||
// farbig, sind nur nicht mehr anwählbar/bearbeitbar.
|
||||
const other = isVisible && !isActive;
|
||||
return { render: isActive || isVisible, greyed: false, locked: other };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -74,6 +74,11 @@ export interface PanelHostValue {
|
||||
onSelectTool: (id: ToolId) => void;
|
||||
/** Werkzeuge nur im Grundriss eines Geschosses sinnvoll. */
|
||||
toolsEnabled: boolean;
|
||||
/** Laufender Engine-Befehl (Highlight der Befehls-Knöpfe im ToolsPanel). */
|
||||
activeCommand: string | null;
|
||||
/** Engine-Befehl starten (Ändern-/BIM-Befehle im ToolsPanel — gleicher Pfad
|
||||
* wie das Tippen des Befehlsnamens in der Befehlszeile). */
|
||||
onRunCommand: (name: string) => void;
|
||||
/** Aktiver Wandtyp (für das Wand-Werkzeug). */
|
||||
activeWallTypeId: string;
|
||||
onActiveWallTypeId: (id: string) => void;
|
||||
@@ -112,6 +117,14 @@ export interface PanelHostValue {
|
||||
* Löschen) und hält das offene Menü.
|
||||
*/
|
||||
onLayerContextMenu: (code: string, clientX: number, clientY: number) => void;
|
||||
/** Öffnet die Ebeneneinstellungen direkt (Zahnrad-Symbol je Zeile) — unter
|
||||
* Tauri als natives Fenster, sonst derselbe InlineEditor-Popover wie über
|
||||
* das Kontextmenü (siehe App.tsx `openLayerSettings`). */
|
||||
onOpenLayerSettings: (code: string, clientX: number, clientY: number) => void;
|
||||
/** Öffnet das Einstellungs-Fenster (Darstellungsfarben, Projekt-Felder) —
|
||||
* unter Tauri als natives Fenster, sonst als In-App-Dialog (App.openSettings).
|
||||
* Als Zahnrad in den Panel-Kopfzeilen „Zeichnungsebenen"/„Ebenen". */
|
||||
onOpenSettings: () => void;
|
||||
|
||||
// Ressourcen-Handler (Bauteile / Schraffuren / Linienstile). Spiegelt
|
||||
// ResourceManagerHandlers, hier flach getippt, damit dieses Modul nicht von
|
||||
@@ -351,6 +364,15 @@ export interface PanelHostValue {
|
||||
* linePoints/depth) — wirkt NUR auf die selektierte Linie.
|
||||
*/
|
||||
onSetSectionLinePatch: (patch: Partial<DrawingLevel>) => void;
|
||||
/**
|
||||
* Ob die aktuell im Objekt-Info gezeigte Schnittlinie (`sectionLine`) den
|
||||
* 3D-Live-Schnitt treibt — der `Wasm3DViewport` clippt dann vertikal entlang
|
||||
* dieser Ebene (`sectionPlaneFromLevel`). Reiner Ansichts-Zustand (nicht im
|
||||
* Dokument).
|
||||
*/
|
||||
section3dCutActive: boolean;
|
||||
/** Bindet den 3D-Live-Schnitt an diese Schnittlinie / hebt die Bindung auf. */
|
||||
onToggleSection3dCut: (id: string) => void;
|
||||
/**
|
||||
* Löscht die Schnittlinie der gewählten Ebene (`linePoints` = undefined); die
|
||||
* Ebene selbst bleibt bestehen (der Schnitt zeigt dann wieder den Hinweis).
|
||||
@@ -368,6 +390,8 @@ export interface PanelHostValue {
|
||||
onSetStairRise: (totalRise: number) => void;
|
||||
/** Setzt die Laufrichtung (aufwärts/abwärts). */
|
||||
onSetStairUp: (up: boolean) => void;
|
||||
/** Setzt die Pfeil-Darstellung (offener Winkel- vs. gefüllter Vollpfeil). */
|
||||
onSetStairArrowStyle: (style: "line" | "filled") => void;
|
||||
/** Setzt den Referenzpunkt der Laufbreite (links/mitte/rechts). */
|
||||
onSetStairReferenz: (referenz: "links" | "mitte" | "rechts") => void;
|
||||
/** Weist der Treppe einen Treppentyp (Bibliothek) zu; "" löst die Zuweisung. */
|
||||
@@ -410,6 +434,8 @@ export interface PanelHostValue {
|
||||
onAddContextObjects: (objs: ContextObject[]) => void;
|
||||
/** Entfernt ein Kontext-Objekt per ID. */
|
||||
onRemoveContextObject: (id: string) => void;
|
||||
/** Entfernt mehrere Kontext-Objekte in einem Schritt (ganze Ebene/Gruppe). */
|
||||
onRemoveContextObjects: (ids: string[]) => void;
|
||||
/**
|
||||
* Erzeugt aus einem ContourSet (per ID) ein Gelände-TIN. Liefert die neue
|
||||
* TerrainMesh-ID oder null (unbekannte ID / kein sinnvolles TIN).
|
||||
@@ -421,7 +447,9 @@ export interface PanelHostValue {
|
||||
* den Bezug (nächster Import setzt ihn automatisch neu).
|
||||
*/
|
||||
onSetGeoAnchor: (
|
||||
anchor: { lv95: { e: number; n: number }; model: { x: number; y: number }; label?: string } | undefined,
|
||||
anchor:
|
||||
| { lv95: { e: number; n: number }; height?: number; model: { x: number; y: number }; label?: string }
|
||||
| undefined,
|
||||
) => void;
|
||||
/** Öffnet den (in App montierten) versteckten DXF-Datei-Dialog. */
|
||||
onImportDxf: () => void;
|
||||
|
||||
+28
-5
@@ -40,7 +40,10 @@ import type {
|
||||
// als weiterer Tab in der rechten unteren Gruppe.
|
||||
// v11: Layouts-Panel ("layouts", DOSSIER A3 / ROADMAP Paragraf 11) als weiterer
|
||||
// Tab in der rechten unteren Gruppe.
|
||||
export const LAYOUT_VERSION = 11;
|
||||
// v12: Werkzeug-Palette ZURÜCK ins Default-Layout (Nutzer-Entscheid: Werkzeuge
|
||||
// gehören in ein andockbares Panel statt ins Ribbon-Band) — wieder oben links,
|
||||
// Attribute darunter (Vectorworks-Bild aus v6, vor dem Ribbon-UI-Phase-3-Bump).
|
||||
export const LAYOUT_VERSION = 12;
|
||||
|
||||
/** localStorage-Schlüssel des zuletzt benutzten Layouts. */
|
||||
const CURRENT_KEY = "cad.layout";
|
||||
@@ -64,11 +67,11 @@ const NAMED_PREFIX = "cad.layouts.";
|
||||
* OBEN, Zeichnungsebenen + Ebenen als Tab-Stapel UNTEN. (Ressourcen bleibt ein
|
||||
* schwebendes Fenster.) Jede Gruppe trägt ein Höhen-Gewicht.
|
||||
*/
|
||||
// Ribbon-UI Phase 3: die Werkzeug-Sidebar ("tools") ist NICHT mehr im Default-
|
||||
// Layout — die Werkzeuge liegen im Ribbon, der Wandtyp-/Deckentyp-Picker im
|
||||
// Attribute-Panel. `ToolsPanel` bleibt registriert (per Fenster-Menü andockbar),
|
||||
// nur nicht mehr per Default sichtbar → Attribute bekommt die volle linke Höhe.
|
||||
// v12: Werkzeug-Palette ("tools") wieder im Default-Layout — oben links, mit
|
||||
// der Attribute-Palette darunter (Vectorworks-Bild). Das Ribbon-Band bleibt als
|
||||
// Reiter erreichbar, ist aber nicht mehr der primäre Weg zu den Werkzeugen.
|
||||
const DEFAULT_LEFT_GROUPS: DockGroup[] = [
|
||||
{ tabs: ["tools"], activeTab: "tools", weight: 1 },
|
||||
{ tabs: ["attributes"], activeTab: "attributes", weight: 1 },
|
||||
];
|
||||
const DEFAULT_RIGHT_GROUPS: DockGroup[] = [
|
||||
@@ -376,6 +379,26 @@ export function setActiveTab(
|
||||
return { ...layout, [dock]: { ...d, groups } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Macht ein Panel sichtbar, gleich wo es gerade steckt — NIE destruktiv (schliesst/
|
||||
* versteckt nie etwas anderes). Für „Fenster > Panels" (native Menüleiste,
|
||||
* appMenu.ts) und künftige ähnliche Einstiege:
|
||||
* • schon schwebend → nach vorne holen (bringToFront).
|
||||
* • angedockt, inaktiver Tab → als aktiven Tab seiner Gruppe setzen.
|
||||
* • nirgends vorhanden → als neues schwebendes Fenster öffnen (Default-Rect).
|
||||
*/
|
||||
export function revealPanel(layout: LayoutState, panelId: string): LayoutState {
|
||||
if (layout.floating.some((f) => f.panelId === panelId)) {
|
||||
return bringToFront(layout, panelId);
|
||||
}
|
||||
for (const dock of ["left", "right"] as DockId[]) {
|
||||
if (layout[dock].groups.some((g) => g.tabs.includes(panelId))) {
|
||||
return setActiveTab(layout, dock, panelId);
|
||||
}
|
||||
}
|
||||
return moveToFloat(layout, panelId, { x: 120, y: 120, w: 320, h: 420 });
|
||||
}
|
||||
|
||||
// `otherDock` wird (noch) nicht extern gebraucht, ist aber Teil der Aufräum-
|
||||
// Semantik; als Helfer exportiert, damit Wiring-Code symmetrisch arbeiten kann.
|
||||
export { otherDock };
|
||||
|
||||
+13
-3
@@ -97,16 +97,18 @@ export interface LayoutState {
|
||||
/**
|
||||
* Darstellungsmodus für ortsabhängige Inhalte (Navigator-Listen, 3D, Grundriss).
|
||||
* Steuert, welche Elemente bezogen auf die aktive Auswahl gezeigt werden — die
|
||||
* fünf DOSSIER-Modi (siehe docs/design/context-menu.md), für Ebenen UND
|
||||
* Zeichnungsebenen identisch:
|
||||
* fünf DOSSIER-Modi (siehe docs/design/context-menu.md) plus eine eigene
|
||||
* Ergänzung ("locked"), für Ebenen UND Zeichnungsebenen identisch:
|
||||
* • "all_force" — alle erzwungen sichtbar; Augen gedimmt; Klick aufs Auge
|
||||
* wechselt zu „Ausgewählte".
|
||||
* • "all" — sichtbar nach per-Zeile-Flag (Standard).
|
||||
* • "active" — nur das aktive Element; andere stark gedimmt.
|
||||
* • "grey" — aktives normal, andere 45 % (Sichtbarkeits-Flags gelten).
|
||||
* • "grey_locked" — wie „grey", andere zusätzlich gesperrt.
|
||||
* • "locked" — aktives normal, andere sichtbare UNVERÄNDERT farbig,
|
||||
* aber gesperrt (nicht anwählbar/bearbeitbar).
|
||||
*/
|
||||
export type DisplayMode = "all_force" | "all" | "active" | "grey" | "grey_locked";
|
||||
export type DisplayMode = "all_force" | "all" | "active" | "grey" | "grey_locked" | "locked";
|
||||
|
||||
// ── Panel-Definition & Kontext ─────────────────────────────────────────────
|
||||
|
||||
@@ -151,6 +153,14 @@ export interface PanelDef {
|
||||
* Kopfzeile anbieten soll. Default: kein Umschalter.
|
||||
*/
|
||||
hasDisplayMode?: boolean;
|
||||
/**
|
||||
* Optionale Aktions-Elemente rechts in der EINEN Kopfzeile des PanelFrame
|
||||
* (z. B. „+ Kategorie", Fenster-Ausklinken). So bündeln Panels ihre Aktionen
|
||||
* in der gemeinsamen Frame-Kopfzeile statt einer zweiten eigenen Titelzeile
|
||||
* (kein doppelter Titel mehr). Wird INNERHALB des PanelHostContext gerendert,
|
||||
* darf also `usePanelHost()` nutzen.
|
||||
*/
|
||||
headerActions?: ReactNode;
|
||||
}
|
||||
|
||||
// ── Host-Context (App-Zustand + Handler für Panels) ────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user