Initial commit — Dossier Rhino 8 Plugin

OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac):
- Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde),
  Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel),
  Treppen (gerade · L · Wendel mit Schrittmass-Validierung)
- Live-Previews mit Step-Lines + Soll-Range-Clamping
- Bidirektionale Selection-Sync zwischen Source-Linie und Volume
- Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz
- Layouts mit PDF-Export
- Ausschnitte / Massstab / Override-Regeln
- Petrol-Gruen Theme (Rapport-konform)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:27:41 +02:00
commit 9dc191be4f
145 changed files with 32629 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
import { useState } from 'react'
import Section from './Section'
import Icon from './Icon'
import GeschossDialog from './GeschossDialog'
import GeschossSettingsDialog from './GeschossSettingsDialog'
function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span>
}
function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) {
// Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention)
const eyeShown = mode !== 'active'
const isGeschoss = !!z.isGeschoss
return (
<div
onClick={onClick}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '4px 12px',
margin: active ? '1px 6px' : '0',
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
// Pill-Form fuer die aktive Zeichnungsebene
borderRadius: active ? 999 : 0,
borderLeft: active ? 'none' : '3px solid transparent',
borderBottom: active ? 'none' : '1px solid var(--border-light)',
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
cursor: 'pointer',
userSelect: 'none',
opacity: (!active && z.visible === false && mode !== 'all') ? 0.45 : 1,
}}
>
<span style={{
fontWeight: active ? 700 : 500,
fontSize: 12,
color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{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 && z.hasClipping && (
<Icon name="content_cut" size={12} style={{ color: 'var(--accent)', flexShrink: 0 }} title="Clipping Plane aktiv" />
)}
{eyeShown ? (
<button
className={`btn-icon-sm ${z.visible !== false ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
title={
active
? (z.visible !== false
? 'Normalerweise sichtbar (aktive Zeichnungsebene wird trotzdem gezeigt)'
: 'Normalerweise ausgeblendet — wird nur sichtbar weil aktiv')
: (z.visible !== false ? 'Ausblenden' : 'Einblenden')
}
><Icon name={z.visible !== false ? 'visibility' : 'visibility_off'} size={14} /></button>
) : (
<span style={{ width: 18, flexShrink: 0 }} />
)}
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onSettings() }}
title="Einstellungen"
><Icon name="settings" size={12} /></button>
</div>
)
}
const MODES = [
{ value: 'all', label: 'Alle anzeigen' },
{ 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 [dialogOpen, setDialogOpen] = useState(false)
const [settingsFor, setSettingsFor] = useState(null) // Geschoss-Objekt oder null
const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen
.filter(z => z.isGeschoss)
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
const addQuick = () => {
const newZ = {
id: `z_${Date.now()}`,
name: `Neu ${zeichnungsebenen.length + 1}`,
isGeschoss: false,
visible: true,
}
onChange([...zeichnungsebenen, newZ])
}
const actions = (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button className="btn-add" onClick={addQuick} title="Zeichnungsebene hinzufügen">
<Icon name="add" size={16} />
</button>
<button className="btn-icon-tonal" onClick={() => setDialogOpen(true)} title="Bearbeiten">
<Icon name="edit" size={14} />
</button>
</div>
)
const toggleVisible = (id) => {
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
}
return (
<>
<Section title="Zeichnungsebenen" badge={zeichnungsebenen.length} action={actions}>
<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>
<select value={mode} onChange={ev => onModeChange(ev.target.value)} style={{ width: '100%' }}>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
</div>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '3px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Gebäudehöhe</span>
<span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>
{gesamthoehe.toFixed(2)} m
</span>
</div>
<div>
{sorted.map(z => (
<ZeichnungsebeneRow
key={z.id}
z={z}
active={z.id === activeId}
mode={mode}
onClick={() => onActiveChange(z.id)}
onToggleVisible={() => toggleVisible(z.id)}
onSettings={() => setSettingsFor(z)}
/>
))}
</div>
</Section>
{dialogOpen && (
<GeschossDialog
zeichnungsebenen={zeichnungsebenen}
recalcOkff={recalcOkff}
onSave={(updated) => { onChange(updated); setDialogOpen(false) }}
onClose={() => setDialogOpen(false)}
/>
)}
{settingsFor && (
<GeschossSettingsDialog
geschoss={settingsFor}
onSave={(updated) => {
onChange(zeichnungsebenen.map(z => z.id === updated.id ? updated : z))
setSettingsFor(null)
}}
onClose={() => setSettingsFor(null)}
/>
)}
</>
)
}