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
+88
View File
@@ -0,0 +1,88 @@
import { useEffect, useRef, useState } from 'react'
import Icon from './Icon'
export default function ContextMenu({ x, y, items, onClose }) {
const ref = useRef(null)
const [pos, setPos] = useState({ left: x, top: y })
// Falls Menue rechts/unten ueberlaufen wuerde, links/oben verschieben
useEffect(() => {
if (!ref.current) return
const rect = ref.current.getBoundingClientRect()
const vw = window.innerWidth
const vh = window.innerHeight
let left = x, top = y
if (left + rect.width > vw - 4) left = vw - rect.width - 4
if (top + rect.height > vh - 4) top = vh - rect.height - 4
if (left < 4) left = 4
if (top < 4) top = 4
setPos({ left, top })
}, [x, y])
useEffect(() => {
const handleClick = (ev) => {
if (ref.current && !ref.current.contains(ev.target)) onClose()
}
const handleKey = (ev) => { if (ev.key === 'Escape') onClose() }
const handleContext = (ev) => { ev.preventDefault(); onClose() }
const t = setTimeout(() => {
document.addEventListener('mousedown', handleClick)
document.addEventListener('keydown', handleKey)
document.addEventListener('contextmenu', handleContext)
}, 0)
return () => {
clearTimeout(t)
document.removeEventListener('mousedown', handleClick)
document.removeEventListener('keydown', handleKey)
document.removeEventListener('contextmenu', handleContext)
}
}, [onClose])
return (
<div
ref={ref}
style={{
position: 'fixed',
top: pos.top, left: pos.left,
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r)',
boxShadow: 'var(--shadow-3)',
padding: '4px 0',
minWidth: 180,
zIndex: 300,
}}
>
{items.map((it, i) => (
it.divider ? (
<div key={i} style={{ height: 1, background: 'var(--border-light)', margin: '4px 0' }} />
) : (
<button
key={i}
disabled={it.disabled}
onClick={() => { if (!it.disabled) { it.onClick(); onClose() } }}
onMouseEnter={(ev) => { if (!it.disabled) ev.currentTarget.style.background = 'var(--overlay-hover)' }}
onMouseLeave={(ev) => { ev.currentTarget.style.background = 'transparent' }}
style={{
width: '100%', textAlign: 'left',
padding: '6px 14px', fontSize: 11,
color: it.disabled ? 'var(--text-muted)'
: it.danger ? 'var(--danger)'
: 'var(--text-primary)',
display: 'flex', alignItems: 'center', gap: 10,
borderRadius: 0,
cursor: it.disabled ? 'default' : 'pointer',
background: 'transparent',
}}
>
{it.icon
? <Icon name={it.icon} size={14} style={{ color: it.disabled ? 'var(--text-muted)' : it.danger ? 'var(--danger)' : 'var(--text-secondary)' }} />
: <span style={{ width: 14 }} />}
<span style={{ flex: 1 }}>{it.label}</span>
{it.shortcut && <span style={{ fontSize: 9, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>{it.shortcut}</span>}
</button>
)
))}
</div>
)
}