import { useState, useEffect, useMemo } from 'react'
import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu'
import {
onMessage, notifyReady,
listAusschnitte, saveAusschnitt, updateAusschnitt,
restoreAusschnitt, applyAusschnittToDetail,
renameAusschnitt, deleteAusschnitt,
setAusschnittFolder, setAusschnittScale,
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
openAusschnittSettings,
} from './lib/rhinoBridge'
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
const [editing, setEditing] = useState(autoEdit || false)
const [val, setVal] = useState(value)
useEffect(() => { setVal(value) }, [value])
useEffect(() => { if (autoEdit) setEditing(true) }, [autoEdit])
const commit = () => {
const trimmed = (val ?? '').trim()
if (trimmed && trimmed !== value) onCommit(trimmed)
else setVal(value)
setEditing(false)
}
if (editing) {
return (
setVal(ev.target.value)}
onBlur={commit}
onKeyDown={(ev) => {
if (ev.key === 'Enter') commit()
if (ev.key === 'Escape') { setVal(value); setEditing(false) }
}}
onClick={(ev) => ev.stopPropagation()}
style={{ ...style, fontSize, padding: '2px 6px' }}
/>
)
}
return (
{ ev.stopPropagation(); setVal(value); setEditing(true) }}
style={{ ...style, fontSize, cursor: 'text' }}
>{value}
)
}
function ScaleCell({ snap, onChange }) {
const [editing, setEditing] = useState(false)
const [val, setVal] = useState(snap.scale || '')
useEffect(() => { setVal(snap.scale || '') }, [snap.scale])
const commit = () => {
onChange(snap.id, val.trim())
setEditing(false)
}
if (editing) {
return (
setVal(ev.target.value)}
onBlur={commit}
onKeyDown={(ev) => {
if (ev.key === 'Enter') commit()
if (ev.key === 'Escape') { setVal(snap.scale || ''); setEditing(false) }
}}
onClick={(ev) => ev.stopPropagation()}
placeholder="1:50"
style={{ width: 64, fontSize: 10, padding: '2px 6px', textAlign: 'right' }}
/>
)
}
return (
{ ev.stopPropagation(); setEditing(true) }}
className={snap.scale ? 'chip chip-accent' : 'chip'}
style={{
fontSize: 9, cursor: 'text',
fontFamily: 'var(--font-mono)',
flexShrink: 0,
}}
title={snap.scale ? `Maßstab ${snap.scale} — wird auf Detail angewendet` : 'Doppelklick um Maßstab einzutragen (z.B. 1:50)'}
>{snap.scale || '—:—'}
)
}
function OrientationBadge({ orientation }) {
const variant = orientation === 'perspective' ? {
icon: 'view_in_ar', color: 'var(--accent)',
title: 'Perspektive',
} : orientation === 'horizontal' ? {
icon: 'align_horizontal_center', color: 'var(--active)',
title: 'Horizontaler Schnitt (Grundriss)',
} : {
icon: 'align_vertical_center', color: 'var(--warn)',
title: 'Vertikaler Schnitt (Schnitt / Ansicht)',
}
return (
)
}
function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, onScaleChange, onDragStart, onDragEnd, dragging }) {
return (
{ ev.currentTarget.style.background = 'var(--bg-item-hover)' }}
onMouseLeave={(ev) => { ev.currentTarget.style.background = 'var(--bg-input)' }}
>
onRename(snap.id, n)}
fontSize={11}
style={{
flex: 1, minWidth: 0,
color: 'var(--text-primary)', fontWeight: 500,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}
/>
)
}
function FolderCard({
name, count, collapsed, onToggle, onContextMenu, onMenuClick,
onDragOver, onDragLeave, onDrop, dragOver, children,
}) {
return (
{name}
{count}
{!collapsed && (
{children || (
Leer — Ausschnitte hier ablegen.
)}
)}
)
}
function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, empty }) {
return (
{children}
)
}
export default function AusschnitteApp() {
const [snaps, setSnaps] = useState([])
const [extraFolders, setExtraFolders] = useState([])
const [newName, setNewName] = useState('')
const [ctxMenu, setCtxMenu] = useState(null)
const [collapsed, setCollapsed] = useState({})
const [draggingId, setDraggingId] = useState(null)
const [dragTarget, setDragTarget] = useState(null)
useEffect(() => {
onMessage('LIST', ({ snapshots, folders }) => {
setSnaps(snapshots || [])
setExtraFolders(folders || [])
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const groups = useMemo(() => {
const map = {}
snaps.forEach(s => {
const f = s.folder || ''
if (!map[f]) map[f] = []
map[f].push(s)
})
return map
}, [snaps])
const allFolders = useMemo(() => {
const set = new Set(extraFolders)
snaps.forEach(s => { if (s.folder) set.add(s.folder) })
return [...set].sort((a, b) => a.localeCompare(b))
}, [snaps, extraFolders])
const handleSave = () => {
const name = newName.trim() || `Ausschnitt ${snaps.length + 1}`
saveAusschnitt(name)
setNewName('')
}
const handleAddFolder = () => {
const name = window.prompt('Name für neuen Ordner:')
if (name && name.trim()) addAusschnittFolder(name.trim())
}
const ctxItems = (id) => [
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
{ divider: true },
{ label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
{ divider: true },
{ label: 'Löschen', icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) },
]
const folderCtxItems = (folderName) => [
{ label: 'Ordner umbenennen', icon: 'edit', onClick: () => {
const newName = window.prompt('Neuer Ordnername:', folderName)
if (newName && newName.trim() && newName !== folderName) {
snaps.filter(s => s.folder === folderName).forEach(s => setAusschnittFolder(s.id, newName.trim()))
addAusschnittFolder(newName.trim())
removeAusschnittFolder(folderName)
}
}},
{ divider: true },
{ label: 'Ordner löschen', icon: 'folder_off', danger: true, onClick: () => {
if (window.confirm(`Ordner "${folderName}" löschen? Ausschnitte werden zur Wurzel verschoben.`)) {
removeAusschnittFolder(folderName)
}
}},
]
const handleDrop = (folderName) => (ev) => {
ev.preventDefault()
setDragTarget(null)
const id = ev.dataTransfer.getData('text/plain') || draggingId
if (id) setAusschnittFolder(id, folderName || '')
setDraggingId(null)
}
const handleDragOver = (folderName) => (ev) => {
ev.preventDefault()
ev.dataTransfer.dropEffect = 'move'
setDragTarget(folderName || 'root')
}
const handleDragLeave = () => () => setDragTarget(null)
const renderSnapshot = (s) => (
restoreAusschnitt(s.id)}
onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' }) }}
onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' })}
onRename={(id, name) => renameAusschnitt(id, name)}
onScaleChange={(id, scale) => setAusschnittScale(id, scale)}
onDragStart={(ev) => {
ev.dataTransfer.setData('text/plain', s.id)
ev.dataTransfer.effectAllowed = 'move'
setDraggingId(s.id)
}}
onDragEnd={() => { setDraggingId(null); setDragTarget(null) }}
/>
)
const rootItems = groups[''] || []
const isEmpty = snaps.length === 0 && allFolders.length === 0
return (
{/* Save-Bar als Card */}
setNewName(ev.target.value)}
onKeyDown={(ev) => { if (ev.key === 'Enter') handleSave() }}
placeholder="Name für neuen Ausschnitt…"
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font)', minWidth: 0 }}
/>
{isEmpty ? (
Noch keine Ausschnitte.
Oben einen Namen eingeben und klicken.
) : (
<>
{/* Root-Snapshots */}
{rootItems.map(s => renderSnapshot(s))}
{rootItems.length === 0 && draggingId && (
Hier ablegen für Wurzel
)}
{/* Ordner-Cards */}
{allFolders.map(folder => {
const isCollapsed = !!collapsed[folder]
const items = groups[folder] || []
return (
setCollapsed(c => ({ ...c, [folder]: !c[folder] }))}
onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' }) }}
onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' })}
onDragOver={handleDragOver(folder)}
onDragLeave={handleDragLeave()}
onDrop={handleDrop(folder)}
>
{items.length > 0 ? items.map(s => renderSnapshot(s)) : null}
)
})}
>
)}
Drag & Drop auf Ordner-Card zum Verschieben · Doppelklick auf Name/Maßstab = bearbeiten · ⋮ für Aktionen
{/* Sticky Footer: Anzahl + Ordner erstellen + Reload */}
{snaps.length}
Ausschnitte
{ctxMenu && (
setCtxMenu(null)}
/>
)}
)
}