Projekt-Settings-Dialog + Library Phase A + Material-Merger

- Project-Settings-Dialog (Voreinstellungen Geschoss/Schnitt + Material-Editor)
  ueber Zahnrad-Icon in Oberleiste; Defaults werden in schnitte.pick_schnitt
  + GeschossManager als Vorgabe genommen, pro-Element-Werte unangetastet
- Dossier-Library Phase A (lokal, read-only): rhino/library.py + LibraryBrowser
  Satellite; Seed-Manifest unter ~/Library/Application Support/Dossier/library/
- Material-Merger: _get_all_materials(doc) merged builtin _MATERIAL_LIBRARY
  mit Projekt-Settings-Materialien (inkl. Library-Imports); Wand-Erstellung,
  Sub-Layer-Anlage + Elemente-Material-Dropdown ziehen jetzt aus dem Merge

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:19:09 +02:00
parent ee01c7ebdc
commit a308ba62d2
13 changed files with 1087 additions and 56 deletions
+32
View File
@@ -0,0 +1,32 @@
import { useState, useEffect } from 'react'
import LibraryBrowser from './components/LibraryBrowser'
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
export default function LibraryApp() {
const [manifest, setManifest] = useState({ items: [] })
const [importedIds, setImportedIds] = useState([])
const [libraryRoot, setLibraryRoot] = useState('')
useEffect(() => {
onMessage('LIBRARY_STATE', ({ manifest, importedIds, libraryRoot }) => {
if (manifest) setManifest(manifest)
if (importedIds) setImportedIds(importedIds)
if (libraryRoot) setLibraryRoot(libraryRoot)
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
return (
<LibraryBrowser
manifest={manifest}
importedIds={importedIds}
libraryRoot={libraryRoot}
onImport={(id) => send('IMPORT_ITEM', { id })}
onReload={() => send('RELOAD', {})}
onClose={() => send('CLOSE', {})}
/>
)
}
+63 -43
View File
@@ -19,6 +19,7 @@ import {
toggleReferenzlinien,
toggleOsnap,
setOsnapMode, toggleGridVisible,
openProjectSettings,
} from './lib/rhinoBridge'
const PRESETS = [
@@ -275,7 +276,7 @@ export default function OberleisteApp() {
</button>
<button
onClick={() => openDossierSettings()}
title="Dossier-Einstellungen"
title="Dossier-Einstellungen (App-Settings, Window-Layout)"
style={{
background: 'transparent', border: 'none', padding: '2px 4px',
cursor: 'pointer', color: 'var(--text-muted)',
@@ -286,6 +287,19 @@ export default function OberleisteApp() {
>
<Icon name="settings" size={14} />
</button>
<button
onClick={() => openProjectSettings()}
title="Projekt-Einstellungen (Voreinstellungen Geschoss/Schnitt + Material-Library)"
style={{
background: 'transparent', border: 'none', padding: '2px 4px',
cursor: 'pointer', color: 'var(--text-muted)',
display: 'flex', alignItems: 'center', flexShrink: 0,
}}
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
>
<Icon name="tune" size={14} />
</button>
<div style={sep} />
{/* ====== VIEW 2x4 Grid ======
Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings)
@@ -709,16 +723,16 @@ export default function OberleisteApp() {
<div style={sep} />
{/* ====== SNAP-BAR (Architektur-Osnaps + Grid) ======
4×2 Grid, nur Icons, schlank. Symbol-Wahl orientiert sich an
Rhinos eigenen Snap-Markern (End=Quadrat, Mid=Dreieck, Cen=
Kreis, Int=X, Perp=Winkel, Near=Plus). Ortho + Grid-Snap sind
in Rhinos Footer-Bar — hier nur was dort fehlt.
Reihe 1: Master-O | End | Mid | Int
Reihe 2: Perp | Cen | Near | Grid */}
Layout: links zwei einzelne Toggle-Buttons (Master-O / Grid)
stacked. Rechts zwei 3-Zellen-Pills mit den Osnap-Modi.
Master-O und Grid sind globale Toggles → eigene Buttons.
Die 6 Modi gehören zusammen → Pill-Gruppe.
Ortho + Grid-Snap sind in Rhinos Footer-Bar — hier nicht. */}
{(() => {
const om = state.osnapModes || {}
const osnapDisabled = !state.osnap
const IconBtn = ({ icon, active, disabled, onClick, isFirst, title }) => (
// Bar-Cell: Teil einer Segment-Pill (kein eigener Border-Radius)
const BarCell = ({ icon, active, disabled, onClick, isFirst, title }) => (
<button onClick={onClick} disabled={disabled} title={title}
onMouseEnter={(e) => {
if (disabled || active) return
@@ -746,53 +760,59 @@ export default function OberleisteApp() {
<Icon name={icon} size={12} />
</button>
)
const rowStyle = {
display: 'inline-flex', width: BAR_H * 4,
const pillRowStyle = {
display: 'inline-flex', width: BAR_H * 3,
height: BAR_H + 2, boxSizing: 'border-box',
border: '1px solid var(--border)', borderRadius: 999,
overflow: 'hidden', flexShrink: 0,
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4, flexShrink: 0,
}}>
<div style={rowStyle}>
<IconBtn icon="gps_fixed" isFirst
<div style={{ display: 'flex', gap: 6, alignItems: 'flex-start',
flexShrink: 0 }}>
{/* Linke Spalte: 2 einzelne Toggle-Buttons stacked */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<BarButton icon="gps_fixed"
active={!!state.osnap}
onClick={() => toggleOsnap(!state.osnap)}
title={state.osnap ? 'Object-Snap an' : 'Object-Snap aus'} />
<IconBtn icon="crop_square" disabled={osnapDisabled}
active={!!om.end}
onClick={() => setOsnapMode('end', !om.end)}
title="Endpunkt (End)" />
<IconBtn icon="change_history" disabled={osnapDisabled}
active={!!om.mid}
onClick={() => setOsnapMode('mid', !om.mid)}
title="Mittelpunkt (Mid)" />
<IconBtn icon="close" disabled={osnapDisabled}
active={!!om.int}
onClick={() => setOsnapMode('int', !om.int)}
title="Schnittpunkt (Intersection)" />
</div>
<div style={rowStyle}>
<IconBtn icon="square_foot" isFirst disabled={osnapDisabled}
active={!!om.perp}
onClick={() => setOsnapMode('perp', !om.perp)}
title="Lotrecht (Perpendicular)" />
<IconBtn icon="radio_button_unchecked" disabled={osnapDisabled}
active={!!om.cen}
onClick={() => setOsnapMode('cen', !om.cen)}
title="Kreis-/Bogen-Mittelpunkt (Center)" />
<IconBtn icon="add" disabled={osnapDisabled}
active={!!om.near}
onClick={() => setOsnapMode('near', !om.near)}
title="Naechster Punkt (Near)" />
<IconBtn
title={state.osnap ? 'Object-Snap an — Klick zum Ausschalten'
: 'Object-Snap aus — Klick zum Einschalten'} />
<BarButton
icon={state.gridVisible === false ? 'grid_off' : 'grid_on'}
active={state.gridVisible !== false}
onClick={() => toggleGridVisible(state.gridVisible === false)}
title="Konstruktions-Raster ein-/ausblenden" />
</div>
{/* Rechte Spalte: 2 Pills mit den 6 Osnap-Modi */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={pillRowStyle}>
<BarCell icon="crop_square" isFirst disabled={osnapDisabled}
active={!!om.end}
onClick={() => setOsnapMode('end', !om.end)}
title="Endpunkt (End)" />
<BarCell icon="change_history" disabled={osnapDisabled}
active={!!om.mid}
onClick={() => setOsnapMode('mid', !om.mid)}
title="Mittelpunkt (Mid)" />
<BarCell icon="close" disabled={osnapDisabled}
active={!!om.int}
onClick={() => setOsnapMode('int', !om.int)}
title="Schnittpunkt (Intersection)" />
</div>
<div style={pillRowStyle}>
<BarCell icon="square_foot" isFirst disabled={osnapDisabled}
active={!!om.perp}
onClick={() => setOsnapMode('perp', !om.perp)}
title="Lotrecht (Perpendicular)" />
<BarCell icon="radio_button_unchecked" disabled={osnapDisabled}
active={!!om.cen}
onClick={() => setOsnapMode('cen', !om.cen)}
title="Kreis-/Bogen-Mittelpunkt (Center)" />
<BarCell icon="add" disabled={osnapDisabled}
active={!!om.near}
onClick={() => setOsnapMode('near', !om.near)}
title="Naechster Punkt (Near)" />
</div>
</div>
</div>
)
})()}
+29
View File
@@ -0,0 +1,29 @@
import { useEffect } from 'react'
import ProjectSettingsDialog from './components/ProjectSettingsDialog'
import { notifyReady } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload })
document.title = 'RHINOMSG::' + json
}
export default function ProjectSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
useEffect(() => {
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
return (
<ProjectSettingsDialog
embedded
initial={initial}
onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})}
/>
)
}
+4 -1
View File
@@ -28,9 +28,11 @@ export default function ZeichnungsebenenApp() {
const [activeId, setActiveId] = useState('eg')
const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN)
const [zMode, setZMode] = useState('active')
const [projectSettings, setProjectSettings] = useState(null)
useEffect(() => {
onMessage('STATE_SYNC', ({ zeichnungsebenen: z }) => {
onMessage('STATE_SYNC', ({ zeichnungsebenen: z, projectSettings: ps }) => {
if (ps) setProjectSettings(ps)
if (z) {
const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r)
const active = r.find(zz => zz.id === activeId) || r[0]
@@ -122,6 +124,7 @@ export default function ZeichnungsebenenApp() {
recalcOkff={recalcOkff}
mode={zMode}
onModeChange={setZMode}
projectSettings={projectSettings}
/>
</div>
</div>
+11 -3
View File
@@ -159,6 +159,7 @@ const MODES = [
export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange,
projectSettings,
}) {
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
@@ -254,16 +255,23 @@ export default function GeschossManager({
return 'Neu'
}
// Project-Default-Hoehe: erst aktives Geschoss, dann erstes Geschoss in
// der Liste, dann hartcodiert 3.0. So uebernimmt jeder neue Eintrag den
// "typischen" Wert des Projekts ohne dass der User irgendwo setzen muss.
// Default-Hoehe: Hierarchie
// 1. Project-Settings (zentrale Voreinstellung — explizit gesetzt)
// 2. Aktives Geschoss (wahrscheinlich gleich vom User gewuenscht)
// 3. Erstes Geschoss in der Liste
// 4. Hartcodiert 3.0
// Project-Settings ueberschreibt alle anderen wenn vorhanden, damit
// der User eine zentrale Vorgabe haben kann.
const projDefaults = projectSettings?.defaults || {}
const defaultGeschossHoehe = () => {
if (projDefaults.geschossHoehe != null) return projDefaults.geschossHoehe
const act = zeichnungsebenen.find(z => z.id === activeId)
if (act?.isGeschoss && act.hoehe != null) return act.hoehe
const first = zeichnungsebenen.find(z => z.isGeschoss && z.hoehe != null)
return first?.hoehe ?? 3.0
}
const defaultSchnitthoehe = () => {
if (projDefaults.schnitthoehe != null) return projDefaults.schnitthoehe
const act = zeichnungsebenen.find(z => z.id === activeId)
if (act?.isGeschoss && act.schnitthoehe != null) return act.schnitthoehe
const first = zeichnungsebenen.find(z => z.isGeschoss && z.schnitthoehe != null)
+202
View File
@@ -0,0 +1,202 @@
import { useState, useMemo } from 'react'
import Icon from './Icon'
/* MaterialCard — Preview-Swatch + Name + Tags + Import-Button. */
function MaterialCard({ item, imported, onImport }) {
const color = item.data?.color || '#888888'
return (
<div style={{
display: 'flex', flexDirection: 'column',
border: '1px solid var(--border)', borderRadius: 6,
background: 'var(--bg-section)',
overflow: 'hidden',
cursor: imported ? 'default' : 'pointer',
opacity: imported ? 0.75 : 1,
}}
onDoubleClick={() => { if (!imported) onImport(item.id) }}
title={imported ? 'Bereits importiert' : 'Doppelklick = importieren'}>
<div style={{
height: 64,
background: color,
borderBottom: '1px solid var(--border-light)',
}} />
<div style={{ padding: '6px 8px', display: 'flex',
flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ flex: 1, fontSize: 11, fontWeight: 600,
color: 'var(--text-primary)',
overflow: 'hidden', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' }}>
{item.name}
</span>
{imported && (
<Icon name="check" size={12}
style={{ color: 'var(--accent)' }} />
)}
</div>
{(item.tags || []).length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{(item.tags || []).slice(0, 4).map(t => (
<span key={t} style={{
fontSize: 9, color: 'var(--text-muted)',
background: 'var(--bg-input)',
padding: '1px 5px', borderRadius: 999,
}}>{t}</span>
))}
</div>
)}
<button
onClick={() => { if (!imported) onImport(item.id) }}
disabled={imported}
style={{
marginTop: 2, padding: '3px 6px', fontSize: 10,
background: imported ? 'transparent' : 'var(--accent)',
color: imported ? 'var(--text-muted)' : '#fff',
border: imported ? '1px solid var(--border)' : 'none',
borderRadius: 999, cursor: imported ? 'default' : 'pointer',
}}>
{imported ? 'Importiert' : 'Importieren'}
</button>
</div>
</div>
)
}
export default function LibraryBrowser({
manifest, importedIds, libraryRoot,
onImport, onReload, onClose,
}) {
const [search, setSearch] = useState('')
const [typeFilter, setTypeFilter] = useState('all')
const items = manifest?.items || []
const importedSet = useMemo(() => new Set(importedIds || []),
[importedIds])
const filtered = useMemo(() => {
const q = search.trim().toLowerCase()
return items.filter(it => {
if (typeFilter !== 'all' && it.type !== typeFilter) return false
if (!q) return true
if ((it.name || '').toLowerCase().includes(q)) return true
if ((it.tags || []).some(t => t.toLowerCase().includes(q))) return true
return false
})
}, [items, search, typeFilter])
const types = useMemo(() => {
const s = new Set(items.map(i => i.type))
return ['all', ...Array.from(s)]
}, [items])
return (
<div style={{
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
}}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 12px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="inventory_2" size={14}
style={{ color: 'var(--accent)', flexShrink: 0 }} />
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
{manifest?.name || 'Dossier-Library'}
</span>
<button onClick={onReload}
title="Manifest neu laden"
style={{ background: 'transparent', border: 'none',
cursor: 'pointer', color: 'var(--text-muted)',
padding: '0 4px' }}>
<Icon name="refresh" size={14} />
</button>
<button onClick={onClose}
style={{ color: 'var(--text-muted)', fontSize: 16,
padding: '0 4px', lineHeight: 1 }}>×</button>
</div>
{/* Toolbar */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border)',
}}>
<input type="text" value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder="Suchen (Name oder Tag)…"
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
borderRadius: 999 }} />
<div style={{ display: 'flex', gap: 0,
border: '1px solid var(--border)',
borderRadius: 999, overflow: 'hidden' }}>
{types.map(t => (
<button key={t}
onClick={() => setTypeFilter(t)}
style={{
background: typeFilter === t ? 'var(--accent)' : 'transparent',
color: typeFilter === t ? '#fff' : 'var(--text-muted)',
border: 'none', padding: '4px 10px', fontSize: 10,
cursor: 'pointer',
}}>
{t === 'all' ? 'Alle' : t}
</button>
))}
</div>
</div>
{/* Grid */}
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
padding: '10px 12px' }}>
{filtered.length === 0 ? (
<div style={{ padding: 24, textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11 }}>
{items.length === 0
? 'Library ist leer. Manifest unter:'
: 'Nichts gefunden — Filter aendern?'}
{items.length === 0 && libraryRoot && (
<div style={{ marginTop: 6, fontSize: 9, fontFamily: 'monospace' }}>
{libraryRoot}/library.json
</div>
)}
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: 8,
}}>
{filtered.map(it => (
<MaterialCard key={it.id}
item={it}
imported={importedSet.has(it.id)}
onImport={onImport} />
))}
</div>
)}
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
fontSize: 9, color: 'var(--text-muted)',
}}>
<span>{filtered.length} / {items.length} Items</span>
<div style={{ flex: 1 }} />
<span title={libraryRoot}
style={{ fontFamily: 'monospace',
overflow: 'hidden', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', maxWidth: 320 }}>
{libraryRoot || ''}
</span>
</div>
</div>
)
}
+288
View File
@@ -0,0 +1,288 @@
import { useState } from 'react'
import Icon from './Icon'
import { openLibrary } from '../lib/rhinoBridge'
/* Inline Field + Tab helpers fuer kompaktes Layout im Satellite. */
function Field({ label, hint, children, style }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3,
padding: '5px 0', ...style }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)',
fontWeight: 500, letterSpacing: 0.2 }}>{label}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
{hint && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>
{hint}
</span>
)}
</div>
)
}
function TabBar({ tabs, active, onChange }) {
return (
<div style={{
display: 'flex', gap: 0,
borderBottom: '1px solid var(--border)',
paddingLeft: 4,
}}>
{tabs.map(t => (
<button key={t.key}
onClick={() => onChange(t.key)}
style={{
background: 'transparent',
border: 'none',
borderBottom: active === t.key ? '2px solid var(--accent)' : '2px solid transparent',
padding: '8px 14px',
fontSize: 11, fontWeight: active === t.key ? 600 : 500,
color: active === t.key ? 'var(--text-primary)' : 'var(--text-muted)',
cursor: 'pointer',
marginBottom: -1,
}}>
{t.label}
</button>
))}
</div>
)
}
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
return (
<div style={{
display: 'grid', gridTemplateColumns: '14px 1fr 90px 60px 18px',
alignItems: 'center', gap: 6,
padding: '4px 8px',
borderBottom: '1px solid var(--border-light)',
background: builtin ? 'var(--bg-section)' : 'transparent',
}}>
<input type="color" value={mat.color || '#888888'}
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
title="Farbe"
style={{ width: 14, height: 14, padding: 0, border: 'none',
background: 'transparent', cursor: 'pointer' }} />
<input type="text" value={mat.name || ''}
onChange={(ev) => onChange({ ...mat, name: ev.target.value })}
disabled={builtin}
placeholder="Name"
style={{ flex: 1, fontSize: 11, minWidth: 0,
opacity: builtin ? 0.7 : 1 }} />
<select value={mat.hatch || 'Solid'}
onChange={(ev) => onChange({ ...mat, hatch: ev.target.value })}
style={{ fontSize: 11, minWidth: 0 }}>
{(hatchPatterns || []).map(h => (
<option key={h} value={h}>{h}</option>
))}
</select>
<input type="number" step="0.1" min="0.1"
value={mat.scale ?? 1.0}
onChange={(ev) => onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })}
title="Hatch-Skalierung"
style={{ fontSize: 11, textAlign: 'right' }} />
{mat.source === 'library' ? (
<span style={{ fontSize: 9, color: 'var(--accent-light)' }}
title="Aus Dossier-Library">L</span>
) : builtin || mat.source === 'builtin' ? (
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}
title="Eingebaut">B</span>
) : (
<button onClick={onDelete} title="Loeschen"
style={{ background: 'transparent', border: 'none',
padding: 0, cursor: 'pointer',
color: 'var(--text-muted)' }}>
<Icon name="close" size={12} />
</button>
)}
</div>
)
}
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 builtin = initial.builtinMaterials || []
const hatchPatterns = initial.hatchPatterns || ['Solid']
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/libraryId vorbereitet fuer kommendes Library-Feature:
// 'local' = manuell erstellt, 'library' = aus Cloud-Repo, libraryId
// referenziert dann das Library-Item per UUID.
source: 'local', libraryId: null,
}],
}))
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 (
<div style={wrapperStyle}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
minHeight: 0, overflow: 'hidden' }}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 12px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="tune" size={14}
style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
Projekt-Einstellungen
</span>
<button onClick={onClose}
style={{ color: 'var(--text-muted)', fontSize: 16,
padding: '0 4px', lineHeight: 1 }}>×</button>
</div>
<TabBar tabs={[
{ key: 'defaults', label: 'Voreinstellungen' },
{ key: 'materials', label: 'Materialien' },
]} active={tab} onChange={setTab} />
{/* Body */}
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
padding: '8px 14px' }}>
{tab === 'defaults' && (
<>
<div style={{ fontSize: 9, color: 'var(--text-muted)',
padding: '4px 0 8px', lineHeight: 1.5 }}>
Diese Werte werden beim Erstellen neuer Elemente als
Voreinstellung genommen. Pro-Element editierte Werte
bleiben davon unberuehrt.
</div>
<Field label="STANDARD-GESCHOSSHÖHE (m)"
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
<input type="number" step="0.05" min="1.0" max="10"
value={draft.defaults.geschossHoehe ?? 3.0}
onChange={(ev) => setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)}
style={{ flex: 1, textAlign: 'right' }} />
</Field>
<Field label="STANDARD-SCHNITTHÖHE (m)"
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses">
<input type="number" step="0.05" min="0.1" max="3"
value={draft.defaults.schnitthoehe ?? 1.0}
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
style={{ flex: 1, textAlign: 'right' }} />
</Field>
<div style={{ height: 1, background: 'var(--border-light)',
margin: '8px 0' }} />
<Field label="STANDARD-SCHNITT-TIEFE HINTEN (m)"
hint="Default-Tiefe für neue Schnitte/Ansichten — wie weit hinter der Schnittlinie sichtbar">
<input type="number" step="0.5" min="0.5"
value={draft.defaults.schnittDepthBack ?? 8.0}
onChange={(ev) => setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)}
style={{ flex: 1, textAlign: 'right' }} />
</Field>
<div style={{ display: 'flex', gap: 6 }}>
<Field label="HÖHE UNTEN (m)" style={{ flex: 1 }}>
<input type="number" step="0.1"
value={draft.defaults.schnittHeightMin ?? -1.0}
onChange={(ev) => setDefault('schnittHeightMin', parseFloat(ev.target.value))}
style={{ flex: 1, textAlign: 'right' }} />
</Field>
<Field label="HÖHE OBEN (m)" style={{ flex: 1 }}>
<input type="number" step="0.1"
value={draft.defaults.schnittHeightMax ?? 12.0}
onChange={(ev) => setDefault('schnittHeightMax', parseFloat(ev.target.value))}
style={{ flex: 1, textAlign: 'right' }} />
</Field>
</div>
</>
)}
{tab === 'materials' && (
<>
<div style={{ fontSize: 9, color: 'var(--text-muted)',
padding: '4px 0 8px', lineHeight: 1.5 }}>
Eingebaute Materialien (B) koennen nicht umbenannt werden,
aber Farbe + Hatch sind anpassbar. Eigene Materialien
koennen frei angelegt werden.
</div>
{/* Built-in materials (read-only name) */}
{builtin.map((m, i) => (
<MaterialRow key={'b_' + m.name}
mat={m}
builtin
hatchPatterns={hatchPatterns}
onChange={() => {/* read-only fuer Phase 1 */}} />
))}
{/* User materials */}
{draft.materials.map((m, i) => (
<MaterialRow key={'u_' + i}
mat={m}
hatchPatterns={hatchPatterns}
onChange={(nm) => setMat(i, nm)}
onDelete={() => delMat(i)} />
))}
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<button onClick={addMat}
style={{
padding: '4px 10px', fontSize: 11,
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999, cursor: 'pointer',
color: 'var(--text-primary)',
display: 'inline-flex', alignItems: 'center', gap: 4,
}}>
<Icon name="add" size={12} /> Material hinzufuegen
</button>
<button onClick={openLibrary}
title="Material aus Dossier-Library importieren"
style={{
padding: '4px 10px', fontSize: 11,
background: 'transparent',
border: '1px solid var(--accent)',
borderRadius: 999, cursor: 'pointer',
color: 'var(--accent)',
display: 'inline-flex', alignItems: 'center', gap: 4,
}}>
<Icon name="inventory_2" size={12} /> Library oeffnen
</button>
</div>
</>
)}
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1 }} />
<button className="btn-text" onClick={onClose}>Abbrechen</button>
<button className="btn-contained" onClick={() => onSave(draft)}>
Übernehmen
</button>
</div>
</div>
</div>
)
}
+4
View File
@@ -185,6 +185,10 @@ export function toggleReferenzlinien(visible) {
// toggleOrtho/toggleGridSnap/toggleOsnap existieren bereits weiter oben.
export function setOsnapMode(key, on) { send('SET_OSNAP_MODE', { key, enabled: !!on }) }
export function toggleGridVisible(on) { send('TOGGLE_GRID_VISIBLE', { visible: !!on }) }
// Projekt-Einstellungen (Voreinstellungen fuer Geschoss/Schnitt + Material-Library)
export function openProjectSettings() { send('OPEN_PROJECT_SETTINGS', {}) }
// Dossier-Library (Material-/Symbol-/Object-Templates, Phase A: lokal+material)
export function openLibrary() { send('OPEN_LIBRARY', {}) }
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
+4
View File
@@ -4,6 +4,8 @@ import './index.css'
import App from './App.jsx'
import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
import ProjectSettingsApp from './ProjectSettingsApp.jsx'
import LibraryApp from './LibraryApp.jsx'
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
import GeschossDialogApp from './GeschossDialogApp.jsx'
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
@@ -39,6 +41,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'elemente' ? ElementeApp
: mode === 'zeichnungsebenen' ? ZeichnungsebenenApp
: mode === 'geschoss_settings' ? GeschossSettingsApp
: mode === 'project_settings' ? ProjectSettingsApp
: mode === 'library' ? LibraryApp
: mode === 'ebenen_settings' ? EbenenSettingsApp
: mode === 'geschoss_dialog' ? GeschossDialogApp
: mode === 'layer_combinations' ? LayerCombinationsApp