Files
DOSSIER/src/components/GeschossManager.jsx
T
karim 0c5f8055a5 Fenster/Tueren LoD + Stile + Phase-3-Ausschnitt-Darstellung + UI-Konsistenz
Fenster/Tueren:
- 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache
  Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen +
  Glas + Sims), detail (1:20, Doppelverglasung).
- Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim
  Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle.
- Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus.
- Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage-
  Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der
  Rahmen-Mittelebene, nicht in Wand-Mitte).
- Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der
  visuelle Indikator fuer die Aussenseite.
- Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets
  (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast).
  Style-ID per UserString am Objekt persistiert. Im Panel BarCombo
  mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"-
  Option zum Picken vor dem Klick.

Ausschnitt-Darstellung (Phase 3):
- Doc-Level Override dossier_aktive_darstellung gewinnt vor per-
  Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer
  regenerate_all_oeffnungen-API.
- Ausschnitt-Capture speichert die Darstellung mit, Restore wendet
  sie an und regeneriert.
- Oberleiste-Quick-Switch BarCombo mit 4 Optionen.
- AusschnittSettings-Dialog: Darstellungs-Dropdown.

Gestaltung (SectionStyle Phase 2):
- _set_section_style schreibt per-Object SectionHatchIndex/Scale/
  Rotation/Color mit Multi-Fallback (Property-Namen varieren je
  Rhino-Build). _selection_summary liest die selben zurueck.
- HatchEditor als shared Component fuer Fill + Section.
- geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion
  (Axis + Volume) als 3D klassifiziert wird.

UI-Konsistenz Panels:
- Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel,
  Modelldarstellung-Dropdown an die freigewordene Position in der
  Oberleiste (Row 1 Col 2 im 2x2-Preset-Block).
- BarCombo erweitert: stretch-Prop (Pill waechst auf Container-
  Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button,
  gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot
  direkt nach dem Caret.
- Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding
  (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye
  16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in
  beiden Panels untereinander ausgerichtet.
- Properties-Container ohne Border (war zuvor accent-gruen, dann
  border — User wollte gar nichts mehr).
- ElementList raus aus dem Elemente-Panel (Uebersicht via Tree-
  Window erreichbar). NeuesElement bleibt voll sichtbar bei
  Selektion (kein Collapse), Properties oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:34:15 +02:00

315 lines
11 KiB
React

import { useState } from 'react'
import Icon from './Icon'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span>
}
function ZeichnungsebeneRow({
z, active, mode, onClick, onContextMenu,
onToggleVisible, onToggleLock, onToggleClipping, onDelete,
}) {
const isGeschoss = !!z.isGeschoss
// Eye-Logik: die aktive Z ist IMMER sichtbar (Backend forciert das), also
// zeigen wir ihr Auge immer als "an" — ohne Ruecksicht aufs visible-Flag.
// Nicht-aktive: in 'all_force' ist visible-Flag ueberschrieben (alle an),
// in 'active' ueberschrieben (alle aus) — Auge dimmt. Sonst (Ausgewaehlte/
// grey) reflektiert es das Flag direkt.
let eyeIcon, eyeOn, eyeOpacity, eyeTitle
if (active) {
eyeIcon = 'visibility'
eyeOn = true
eyeOpacity = 1
eyeTitle = z.visible !== false
? 'Sichtbar (aktive Zeichnungsebene)'
: 'Normalerweise ausgeblendet — wird gezeigt weil aktiv'
} else if (mode === 'all_force') {
eyeIcon = 'visibility'
eyeOn = true
eyeOpacity = 0.35
eyeTitle = 'Im „Alle anzeigen"-Mode immer sichtbar — Klick wechselt in „Ausgewählte"'
} else if (mode === 'active') {
eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off'
eyeOn = false
eyeOpacity = 0.35
eyeTitle = 'Im „Nur aktive"-Mode ausgeblendet — Klick wechselt in „Ausgewählte"'
} else {
eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off'
eyeOn = z.visible !== false
eyeOpacity = 1
eyeTitle = z.visible !== false ? 'Ausblenden' : 'Einblenden'
}
return (
<div
onClick={onClick}
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px 1px 0',
margin: 0,
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
borderRadius: active ? 999 : 0,
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
cursor: 'pointer',
userSelect: 'none',
minHeight: 24,
}}
>
{/* Spacer-Slot — spiegelt den Chevron-Slot bei Ebenen-Rows wider
damit die Eye-Icons beider Panels untereinander stehen. */}
<span style={{ width: 12, flexShrink: 0 }} />
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
title={eyeTitle}
style={{ opacity: eyeOpacity, width: 16, height: 16 }}
><Icon name={eyeIcon} size={12} /></button>
<span style={{
fontWeight: 500,
fontSize: 11,
color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
lineHeight: 1.2,
}}>{z.name}</span>
{isGeschoss && (
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
+{(z.okff ?? 0).toFixed(2)}
</span>
)}
{isGeschoss && <GeschossBadge name={z.name} />}
{isGeschoss ? (
<button
className={`btn-icon-xs ${z.hasClipping ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleClipping() }}
title={z.hasClipping
? 'Clipping Plane ausschalten'
: 'Clipping Plane einschalten (Schnitt auf Schnitthöhe)'}
style={{ color: z.hasClipping ? 'var(--accent)' : undefined, width: 14, height: 14 }}
><Icon name="content_cut" size={11} /></button>
) : (
<span style={{ width: 14, flexShrink: 0 }} />
)}
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
title={z.locked ? 'Entsperren' : 'Sperren'}
style={{ color: z.locked ? 'var(--warn)' : undefined, width: 14, height: 14 }}
><Icon name={z.locked ? 'lock' : 'lock_open'} size={11} /></button>
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
title="Löschen"
style={{ width: 14, height: 14 }}
><Icon name="close" size={11} /></button>
</div>
)
}
const MODES = [
{ value: 'all_force', label: 'Alle anzeigen' },
{ value: 'all', label: 'Ausgewählte' },
{ value: 'active', label: 'Nur aktive' },
{ value: 'grey', label: 'Andere grau' },
{ value: 'grey_locked', label: 'Andere grau & gesperrt' },
]
export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange,
}) {
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
const sorted = [...zeichnungsebenen].reverse()
const addQuick = () => {
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
// Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss
// umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen.
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
const newZ = {
id: `z_${Date.now()}`,
name: `Zeichnung ${nonGeschossCount + 1}`,
isGeschoss: false,
visible: true,
}
onChange([...zeichnungsebenen, newZ])
}
const toggleVisible = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
// In "active" / "all_force" greift visible-Flag nicht — wer aufs Auge
// klickt will offensichtlich Sichtbarkeit kontrollieren, also direkt
// in den "Ausgewählte"-Mode wechseln damit die Aktion wirkt.
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}
const toggleLock = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : z))
}
const toggleClipping = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, hasClipping: !z.hasClipping } : z))
}
const duplicate = (id) => {
const src = zeichnungsebenen.find(z => z.id === id)
if (!src) return
const clone = {
...src,
id: `z_${Date.now()}`,
name: `${src.name} Kopie`,
}
// Direkt nach dem Original einfuegen
const idx = zeichnungsebenen.findIndex(z => z.id === id)
const next = [...zeichnungsebenen]
next.splice(idx + 1, 0, clone)
onChange(next)
}
const remove = (id) => {
if (zeichnungsebenen.length <= 1) return
const target = zeichnungsebenen.find(z => z.id === id)
if (!target) return
if (!window.confirm(`"${target.name}" wirklich löschen?`)) return
onChange(zeichnungsebenen.filter(z => z.id !== id))
if (activeId === id) {
const next = zeichnungsebenen.find(z => z.id !== id)
if (next) onActiveChange(next.id)
}
}
const openContextMenu = (ev, id) => {
ev.preventDefault(); ev.stopPropagation()
setCtxMenu({ x: ev.clientX, y: ev.clientY, id })
}
const ctxItems = (id) => {
const z = zeichnungsebenen.find(x => x.id === id)
if (!z) return []
return [
{ label: 'Einstellungen…', icon: 'settings', onClick: () => openGeschossSettings(z) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicate(id) },
{ divider: true },
{ label: 'Löschen', icon: 'delete', danger: true,
disabled: zeichnungsebenen.length <= 1,
onClick: () => remove(id) },
]
}
return (
<>
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
onGear={() => openGeschossDialog(zeichnungsebenen)}
gearIcon="settings"
gearTitle="Einstellungen"
onSecond={addQuick}
secondIcon="add"
secondTitle="Zeichnungsebene hinzufügen"
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
</div>
</div>
{/* Master-Row analog EbenenManager. Padding + Icon-Sizes identisch
damit beide Panels visuell kohaerent sind. Erste 12px-Spanne
spiegelt den Chevron-Slot der Ebenen-Daten-Rows wider. */}
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 12px 2px 0',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ width: 12, flexShrink: 0 }} />
<button
className="btn-icon-xs"
onClick={() => {
const anyVisible = zeichnungsebenen.some(z => z.visible !== false)
onChange(zeichnungsebenen.map(z => ({ ...z, visible: !anyVisible })))
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={zeichnungsebenen.every(z => z.visible !== false)
? 'Alle Zeichnungsebenen ausblenden'
: 'Alle Zeichnungsebenen einblenden'}
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={zeichnungsebenen.every(z => z.visible !== false) ? 'visibility' : 'visibility_off'}
size={11}
/>
</button>
<span style={{ flex: 1 }} />
<button
className="btn-icon-xs"
onClick={() => {
const anyLocked = zeichnungsebenen.some(z => z.locked === true)
onChange(zeichnungsebenen.map(z => ({ ...z, locked: !anyLocked })))
}}
title={zeichnungsebenen.every(z => z.locked === true)
? 'Alle Zeichnungsebenen entsperren'
: 'Alle Zeichnungsebenen sperren'}
style={{ width: 14, height: 14 }}
>
<Icon
name={zeichnungsebenen.every(z => z.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
</button>
<div style={{ width: 14 }} />
</div>
<div>
{sorted.map(z => (
<ZeichnungsebeneRow
key={z.id}
z={z}
active={z.id === activeId}
mode={mode}
onClick={() => onActiveChange(z.id)}
onContextMenu={(ev) => openContextMenu(ev, z.id)}
onToggleVisible={() => toggleVisible(z.id)}
onToggleLock={() => toggleLock(z.id)}
onToggleClipping={() => toggleClipping(z.id)}
onDelete={() => remove(z.id)}
/>
))}
</div>
{ctxMenu && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
items={ctxItems(ctxMenu.id)}
onClose={() => setCtxMenu(null)}
/>
)}
</>
)
}