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 (
)
}
/* 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 (
)
}
/* 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 */}
{!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, hatch: ev.target.value })}
style={{ flex: 1, height: BAR_H, fontSize: 11 }}>
{(hatchPatterns || []).map(h => (
{h}
))}
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 */}
)
}