import { useState, useEffect } from 'react' import Icon from './Icon' import { BarToggle, BarButton, BAR_H } from './BarControls' import { openLibrary, pickTextureFile, onMessage } from '../lib/rhinoBridge' /* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */ function Field({ label, hint, children, style }) { return (
{label}
{children}
{hint && ( {hint} )}
) } /* InlineNumberField — Label links, schmales Number-Input rechts (kompakt) */ function InlineNumberField({ label, hint, value, onChange, step, min, max, suffix }) { return (
{label} onChange(parseFloat(ev.target.value))} style={{ width: 80, height: BAR_H, padding: '0 10px', fontSize: 11, textAlign: 'right' }} /> {suffix && ( {suffix} )}
{hint && (
{hint}
)}
) } /* Pill-Tabs — gleicher Stil wie BarToggle aus der Oberleiste */ function TabBar({ tabs, active, onChange }) { return (
{tabs.map(t => ( onChange(t.key)} /> ))}
) } /* MaterialListRow — schmale Listen-Zeile links (ArchiCAD-Stil): Color-Swatch + Name + Source-Badge. Click selektiert. */ function MaterialListRow({ mat, isBuiltin, isSelected, onSelect }) { const isLibrary = mat.source === 'library' return (
{ if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)' }} onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = 'transparent' }}>
{mat.name || 'Unbenannt'} {isLibrary ? 'L' : isBuiltin ? 'B' : ''}
) } /* Slider — Pill-Track mit Accent-Fill, Wert rechts daneben */ function Slider({ label, value, onChange, min = 0, max = 1, step = 0.01, display, disabled }) { const v = value ?? min const pct = ((v - min) / (max - min)) * 100 return (
{label} {display !== undefined ? display : v.toFixed(2)}
onChange(parseFloat(ev.target.value))} style={{ width: '100%', marginTop: 4, accentColor: 'var(--accent)', opacity: disabled ? 0.5 : 1, }} />
) } /* TextureSlot — Datei-Picker + Vorschau-Mini-Tile + Clear-Button. tex: {path: string} oder null */ function TextureSlot({ label, slot, tex, onChange, disabled }) { const hasPath = !!(tex && tex.path) const filename = hasPath ? tex.path.split('/').pop() : '' return (
{!hasPath && ( )}
{label}
{hasPath ? filename : 'Keine Textur'}
pickTextureFile(slot)} title="Datei waehlen" disabled={disabled} /> {hasPath && ( onChange(null)} title="Textur entfernen" disabled={disabled} /> )}
) } /* DetailSection — Section-Header + Body, immer offen (collapsible spaeter) */ function DetailSection({ title, children }) { return (
{title}
{children}
) } /* MaterialDetail — rechte Seite (ArchiCAD-Stil): editiert das aktuell ausgewaehlte Material. Builtin: Name read-only. */ function MaterialDetail({ mat, isBuiltin, hatchPatterns, onChange, onDelete }) { if (!mat) { return (
Kein Material ausgewaehlt.
Wähle links oder lege ein neues an.
) } const isLibrary = mat.source === 'library' return (
{/* Identitaet */}
onChange({ ...mat, name: ev.target.value })} disabled={isBuiltin} placeholder="Material-Name" style={{ width: '100%', height: BAR_H, padding: '0 12px', fontSize: 12, fontWeight: 500, opacity: isBuiltin ? 0.7 : 1 }} />
{isLibrary ? 'Aus Dossier-Library' : isBuiltin ? 'Eingebaut (Builtin)' : 'Lokales Material'}
{!isBuiltin && onDelete && ( )}
onChange({ ...mat, color: ev.target.value })} title="Farbe" style={{ width: 32, height: BAR_H, padding: 0, border: 'none', background: 'transparent', cursor: 'pointer' }} /> onChange({ ...mat, color: ev.target.value })} style={{ flex: 1, height: BAR_H, padding: '0 12px', fontSize: 11, fontFamily: 'var(--font-mono)' }} />
onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })} title="Skalierung" style={{ width: 70, height: BAR_H, padding: '0 10px', fontSize: 11, textAlign: 'right' }} />
Hatch-Pattern + Skalierung fuer die Sektion-Ansicht (Clipping Plane).
onChange({ ...mat, textures: { ...(mat.textures || {}), diffuse: t } })} disabled={isBuiltin} /> onChange({ ...mat, textures: { ...(mat.textures || {}), bump: t } })} disabled={isBuiltin} /> onChange({ ...mat, textures: { ...(mat.textures || {}), roughness: t } })} disabled={isBuiltin} /> onChange({ ...mat, textures: { ...(mat.textures || {}), transparency: t } })} disabled={isBuiltin} />
onChange({ ...mat, uvScaleM: v || 1.0 })} hint="1 Welt-Meter ≙ wieviel Textur-Tile" />
onChange({ ...mat, roughness: v })} disabled={isBuiltin} /> onChange({ ...mat, reflection: v })} disabled={isBuiltin} /> onChange({ ...mat, transparency: v })} disabled={isBuiltin} /> onChange({ ...mat, iorN: v })} disabled={isBuiltin} />
) } export default function ProjectSettingsDialog({ initial, onSave, onClose, embedded = false, }) { const [tab, setTab] = useState('defaults') const [draft, setDraft] = useState(() => ({ defaults: { ...(initial.defaults || {}) }, materials: [...(initial.materials || [])], })) const [selMat, setSelMat] = useState(() => { // Default-Auswahl: erstes Builtin wenn vorhanden, sonst erstes Local const b = initial.builtinMaterials || [] if (b.length) return { kind: 'builtin', name: b[0].name } const m = initial.materials || [] if (m.length) return { kind: 'local', idx: 0 } return null }) const [matSearch, setMatSearch] = useState('') const builtin = initial.builtinMaterials || [] const hatchPatterns = initial.hatchPatterns || ['Solid'] // Aktuell ausgewaehltes Material aus Selection ableiten const selectedMat = (() => { if (!selMat) return null if (selMat.kind === 'builtin') return builtin.find(m => m.name === selMat.name) || null if (selMat.kind === 'local') return draft.materials[selMat.idx] || null return null })() const selectedIsBuiltin = selMat?.kind === 'builtin' const updateSelected = (newMat) => { if (!selMat) return if (selMat.kind === 'local') { setMat(selMat.idx, newMat) } // builtin: Schreibend in Phase 1 nur Color/Hatch — Backend ignoriert // Name-Aenderungen. UI laesst diese sowieso disabled. } const deleteSelected = () => { if (selMat?.kind !== 'local') return delMat(selMat.idx) setSelMat(null) } // Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell // selektierten Material. Wenn path leer = User abgebrochen → no-op. useEffect(() => { onMessage('TEXTURE_PICKED', ({ slot, path }) => { if (!path || !selMat || selMat.kind !== 'local') return setDraft(d => ({ ...d, materials: d.materials.map((m, i) => { if (i !== selMat.idx) return m const newTex = (m.textures && typeof m.textures === 'object') ? { ...m.textures } : {} newTex[slot] = { path } return { ...m, textures: newTex } }), })) }) }, [selMat]) // Suchbar — case-insensitive substring auf Name const matchSearch = (m) => { const q = matSearch.trim().toLowerCase() if (!q) return true return (m.name || '').toLowerCase().includes(q) } const filteredBuiltin = builtin.filter(matchSearch) const filteredLocal = draft.materials .map((m, i) => ({ m, i })) .filter(({ m }) => matchSearch(m)) const setDefault = (k, v) => setDraft(d => ({ ...d, defaults: { ...d.defaults, [k]: v } })) const setMat = (i, newMat) => setDraft(d => ({ ...d, materials: d.materials.map((m, idx) => idx === i ? newMat : m), })) const delMat = (i) => setDraft(d => ({ ...d, materials: d.materials.filter((_, idx) => idx !== i), })) const addMat = () => { setDraft(d => ({ ...d, materials: [...d.materials, { name: 'Neues Material', color: '#aaaaaa', hatch: 'Solid', scale: 1.0, source: 'local', libraryId: null, }], })) // Direkt selektieren — User kann gleich editieren setSelMat({ kind: 'local', idx: draft.materials.length }) } const wrapperStyle = embedded ? { width: '100%', height: '100%', background: 'var(--bg-dialog)', display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11, } : { position: 'absolute', inset: 0, zIndex: 150, background: 'var(--bg-overlay)', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: 40, } return (
{/* Body */}
{tab === 'defaults' && (
Voreinstellungen fuer neue Elemente. Pro-Element editierte Werte bleiben davon unberuehrt.
setDefault('geschossHoehe', v || 3.0)} hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar" /> setDefault('schnitthoehe', v || 1.0)} hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses" /> setDefault('schnittDepthBack', v || 8.0)} hint="Default-Tiefe fuer neue Schnitte/Ansichten" /> setDefault('schnittHeightMin', v)} /> setDefault('schnittHeightMax', v)} />
)} {tab === 'materials' && (
{/* Links: Liste */}
setMatSearch(ev.target.value)} placeholder="Suchen…" style={{ width: '100%', height: BAR_H, padding: '0 12px', fontSize: 11, boxSizing: 'border-box' }} />
{filteredBuiltin.length > 0 && (
Eingebaut
)} {filteredBuiltin.map((m) => ( setSelMat({ kind: 'builtin', name: m.name })} /> ))} {filteredLocal.length > 0 && (
Projekt
)} {filteredLocal.map(({ m, i }) => ( setSelMat({ kind: 'local', idx: i })} /> ))} {filteredBuiltin.length === 0 && filteredLocal.length === 0 && (
Nichts gefunden.
)}
{/* Rechts: Detail */}
)}
{/* Footer — Pill-Buttons */}
onSave(draft)} />
) }