Library Phase A.2 (Symbol/Object-Import) + Oberleiste-Pill-Restyle
Library Phase A.2: - import_symbol/import_object via File3dm.Read + InstanceDefinitions.Add - Stabile Block-Namen 'dossier_lib_<libraryId>' fuer Dedupe - Seed-Manifest erweitert um Nordpfeil (symbol) + Laubbaum (object) - ItemCard rendert type-spezifische Preview (Color-Swatch fuer material, Material-Icon fuer symbol/object) Oberleiste-Pill-Restyle: - OberleisteApp: Version unter DOSSIER-Logo, Settings-Icons vertikal gestapelt - ProjectSettingsDialog: Pill-Tabs, BarToggle-Footer, MaterialRow mit Hover-Highlight, Header entfernt (Eto.Form hat eigenen) - LibraryBrowser: BarButton-Reload, Pill-Typ-Filter, MaterialCard mit BarToggle-Pill, Header entfernt - Globaler select-Stil: bg-input statt bg-item (dunkler im Dark-Mode, konsistent zu Oberleiste-BarCombo) Routing: - OberleisteBridge delegiert OPEN_PROJECT_SETTINGS + OPEN_LIBRARY an EbenenBridge (sticky ebenen_bridge_ref) — vorher kamen die Messages an der falschen Bridge an und wurden verschluckt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+154
-5
@@ -70,7 +70,9 @@ def ensure_library():
|
|||||||
|
|
||||||
|
|
||||||
def _write_seed_manifest(path):
|
def _write_seed_manifest(path):
|
||||||
"""Bootstrappt 4 typische Architektur-Materialien als Start."""
|
"""Bootstrappt 4 Materialien + 2 Symbol/Object-Beispiel-Eintraege als
|
||||||
|
Start. Die Symbol/Object-Files muss der User selber ablegen unter
|
||||||
|
library/assets/ — sonst schlaegt der Import fehl (graceful)."""
|
||||||
seed = {
|
seed = {
|
||||||
"schemaVersion": _SCHEMA_VERSION,
|
"schemaVersion": _SCHEMA_VERSION,
|
||||||
"name": "Dossier-Library (lokal)",
|
"name": "Dossier-Library (lokal)",
|
||||||
@@ -103,6 +105,20 @@ def _write_seed_manifest(path):
|
|||||||
"tags": ["holz", "ausbau"],
|
"tags": ["holz", "ausbau"],
|
||||||
"data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0},
|
"data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "sym-nordpfeil-01",
|
||||||
|
"type": "symbol", "version": 1,
|
||||||
|
"name": "Nordpfeil",
|
||||||
|
"tags": ["plan", "nordpfeil"],
|
||||||
|
"files": ["assets/sym-nordpfeil-01.3dm"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "obj-baum-laubbaum-01",
|
||||||
|
"type": "object", "version": 1,
|
||||||
|
"name": "Baum — Laubbaum",
|
||||||
|
"tags": ["aussen", "vegetation"],
|
||||||
|
"files": ["assets/obj-baum-laubbaum-01.3dm"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
@@ -148,6 +164,9 @@ def _normalize_item(it):
|
|||||||
"tags": list(it.get("tags") or []),
|
"tags": list(it.get("tags") or []),
|
||||||
"preview": it.get("preview"),
|
"preview": it.get("preview"),
|
||||||
"data": it.get("data") or {},
|
"data": it.get("data") or {},
|
||||||
|
# files: relative Pfade (zur library_root()) auf .3dm-Fragmente.
|
||||||
|
# Symbol/Object-Import liest die ueber File3dm.Read.
|
||||||
|
"files": list(it.get("files") or []),
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -191,13 +210,143 @@ def import_material(doc, item):
|
|||||||
return True, "Material importiert"
|
return True, "Material importiert"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Symbol/Object-Import via File3dm + InstanceDefinitions ----------------
|
||||||
|
|
||||||
|
def _lib_asset_path(rel_path):
|
||||||
|
"""Absoluter Pfad zu einer Library-Asset-Datei (Eintrag aus item.files)."""
|
||||||
|
if not rel_path: return None
|
||||||
|
return os.path.join(library_root(), rel_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _block_name_for(item):
|
||||||
|
"""Stabiler Block-Name fuer InstanceDefinition. Format:
|
||||||
|
'dossier_lib_<libraryId>' — Dedup ueber Lib-ID, nicht Item-Name (sonst
|
||||||
|
Konflikt bei Umbenennen)."""
|
||||||
|
lid = item.get("id") or ""
|
||||||
|
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
|
||||||
|
return "dossier_lib_" + safe
|
||||||
|
|
||||||
|
|
||||||
|
def _read_3dm_geometry(abs_path):
|
||||||
|
"""Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von
|
||||||
|
(GeometryBase, ObjectAttributes). Bei Fehler leere Liste."""
|
||||||
|
if not abs_path or not os.path.isfile(abs_path):
|
||||||
|
print("[LIBRARY] _read_3dm: Datei nicht gefunden:", abs_path)
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
from Rhino.FileIO import File3dm
|
||||||
|
f3 = File3dm.Read(abs_path)
|
||||||
|
if f3 is None:
|
||||||
|
print("[LIBRARY] _read_3dm: File3dm.Read returned None:", abs_path)
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for obj in f3.Objects:
|
||||||
|
try:
|
||||||
|
g = obj.Geometry
|
||||||
|
a = obj.Attributes.Duplicate() if obj.Attributes else None
|
||||||
|
if g is not None:
|
||||||
|
out.append((g.Duplicate(), a))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] _read_3dm obj:", ex)
|
||||||
|
return out
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] _read_3dm:", ex)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_block_definition(doc, item, geometry_attrs):
|
||||||
|
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
|
||||||
|
da. Returns (idx, was_created). idx<0 bei Fehler."""
|
||||||
|
if Rhino is None: return -1, False
|
||||||
|
name = _block_name_for(item)
|
||||||
|
try:
|
||||||
|
existing = doc.InstanceDefinitions.Find(name)
|
||||||
|
except Exception:
|
||||||
|
existing = None
|
||||||
|
if existing is not None:
|
||||||
|
return existing.Index, False
|
||||||
|
# Geometry + Attributes separat sammeln
|
||||||
|
geoms = [g for g, _ in geometry_attrs if g is not None]
|
||||||
|
attrs = [a for _, a in geometry_attrs]
|
||||||
|
if not geoms:
|
||||||
|
return -1, False
|
||||||
|
base_pt = Rhino.Geometry.Point3d(0, 0, 0)
|
||||||
|
desc = "Dossier-Library: " + (item.get("name") or "")
|
||||||
|
try:
|
||||||
|
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
|
||||||
|
return idx, True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] InstanceDef Add:", ex)
|
||||||
|
return -1, False
|
||||||
|
|
||||||
|
|
||||||
|
def import_symbol(doc, item):
|
||||||
|
"""Importiert ein Symbol-Item (= 2D-Block) in das Doc. Liest die
|
||||||
|
.3dm-Datei(en) aus item.files, erstellt eine InstanceDefinition mit
|
||||||
|
stabilem Namen, fuegt eine Instanz am Ursprung ein.
|
||||||
|
Returns (ok, message)."""
|
||||||
|
return _import_block_like(doc, item, kind="symbol")
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(doc, item):
|
||||||
|
"""Importiert ein Object-Item (= 3D-Block, BIM-Element) in das Doc.
|
||||||
|
Gleiche Pipeline wie import_symbol — Symbol/Object unterscheiden sich
|
||||||
|
nur in der UI-Kategorisierung."""
|
||||||
|
return _import_block_like(doc, item, kind="object")
|
||||||
|
|
||||||
|
|
||||||
|
def _import_block_like(doc, item, kind):
|
||||||
|
if doc is None: return False, "Kein aktives Dokument"
|
||||||
|
files = item.get("files") or []
|
||||||
|
if not files:
|
||||||
|
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
|
||||||
|
# Alle Files zusammen in eine Block-Definition packen.
|
||||||
|
all_geom = []
|
||||||
|
for f in files:
|
||||||
|
abs_p = _lib_asset_path(f)
|
||||||
|
all_geom.extend(_read_3dm_geometry(abs_p))
|
||||||
|
if not all_geom:
|
||||||
|
return False, "Keine importierbare Geometrie in {}".format(files)
|
||||||
|
idx, created = _ensure_block_definition(doc, item, all_geom)
|
||||||
|
if idx < 0:
|
||||||
|
return False, "InstanceDefinition konnte nicht erstellt werden"
|
||||||
|
# Instanz am Ursprung einfuegen — User kann danach verschieben.
|
||||||
|
try:
|
||||||
|
xform = Rhino.Geometry.Transform.Identity
|
||||||
|
inst_id = doc.Objects.AddInstanceObject(idx, xform)
|
||||||
|
if inst_id == System_Guid_Empty():
|
||||||
|
return False, "AddInstanceObject fehlgeschlagen"
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
msg = ("Block '{}' importiert + am Ursprung eingefuegt".format(
|
||||||
|
item.get("name") or "")
|
||||||
|
if created else
|
||||||
|
"Block bereits vorhanden — neue Instanz eingefuegt")
|
||||||
|
return True, msg
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] AddInstanceObject:", ex)
|
||||||
|
return False, str(ex)
|
||||||
|
|
||||||
|
|
||||||
|
def System_Guid_Empty():
|
||||||
|
"""Helper — System.Guid.Empty Vergleichswert."""
|
||||||
|
try:
|
||||||
|
import System
|
||||||
|
return System.Guid.Empty
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def import_item(doc, item_id):
|
def import_item(doc, item_id):
|
||||||
"""Type-dispatching Import. Phase A: nur material. symbol/object kommen
|
"""Type-dispatching Import. material → Project-Settings-Liste.
|
||||||
spaeter via File3dm.Read + InstanceDefinition."""
|
symbol/object → InstanceDefinition im Doc via File3dm.Read."""
|
||||||
item = find_item(item_id)
|
item = find_item(item_id)
|
||||||
if item is None: return False, "Item nicht gefunden: " + str(item_id)
|
if item is None: return False, "Item nicht gefunden: " + str(item_id)
|
||||||
t = item.get("type")
|
t = item.get("type")
|
||||||
if t == "material":
|
if t == "material":
|
||||||
return import_material(doc, item)
|
return import_material(doc, item)
|
||||||
# Phase A: symbol/object noch nicht
|
if t == "symbol":
|
||||||
return False, "Typ '{}' wird erst in Phase A.2 unterstuetzt".format(t)
|
return import_symbol(doc, item)
|
||||||
|
if t == "object":
|
||||||
|
return import_object(doc, item)
|
||||||
|
return False, "Unbekannter Typ: '{}'".format(t)
|
||||||
|
|||||||
@@ -1066,6 +1066,22 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
masse_settings.open_as_window()
|
masse_settings.open_as_window()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] open masse:", ex)
|
print("[OBERLEISTE] open masse:", ex)
|
||||||
|
elif t == "OPEN_PROJECT_SETTINGS":
|
||||||
|
# Delegiert an EBENEN-Bridge (sie haelt die Satellite-Logik fuer
|
||||||
|
# Projekt-Settings + Library).
|
||||||
|
try:
|
||||||
|
eb = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if eb is not None: eb._open_project_settings()
|
||||||
|
else: print("[OBERLEISTE] open project-settings: ebenen_bridge_ref nicht da")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] open project-settings:", ex)
|
||||||
|
elif t == "OPEN_LIBRARY":
|
||||||
|
try:
|
||||||
|
eb = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if eb is not None: eb._open_library()
|
||||||
|
else: print("[OBERLEISTE] open library: ebenen_bridge_ref nicht da")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] open library:", ex)
|
||||||
|
|
||||||
# --- Darstellung (SIA-400 LoD globaler Override) -----------------
|
# --- Darstellung (SIA-400 LoD globaler Override) -----------------
|
||||||
elif t == "SET_DARSTELLUNG":
|
elif t == "SET_DARSTELLUNG":
|
||||||
|
|||||||
+20
-8
@@ -253,12 +253,13 @@ export default function OberleisteApp() {
|
|||||||
overflowX: 'auto', overflowY: 'hidden',
|
overflowX: 'auto', overflowY: 'hidden',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{/* Logo: DOSSIER. (Petrol-Punkt) — Klick = About-Fenster */}
|
{/* Logo: DOSSIER. + Version darunter (Klick = About-Fenster) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => openAbout()}
|
onClick={() => openAbout()}
|
||||||
title="Über Dossier"
|
title="Über Dossier"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'baseline', gap: 8,
|
display: 'flex', flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start', gap: 0,
|
||||||
flexShrink: 0, userSelect: 'none',
|
flexShrink: 0, userSelect: 'none',
|
||||||
background: 'transparent', border: 'none', padding: 0,
|
background: 'transparent', border: 'none', padding: 0,
|
||||||
cursor: 'pointer', color: 'inherit',
|
cursor: 'pointer', color: 'inherit',
|
||||||
@@ -273,33 +274,44 @@ export default function OberleisteApp() {
|
|||||||
}}>
|
}}>
|
||||||
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
fontSize: 8, letterSpacing: '0.05em',
|
||||||
|
color: 'var(--text-muted)', lineHeight: 1.4, marginTop: 2,
|
||||||
|
}}>
|
||||||
|
v{__APP_VERSION__}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
{/* Settings-Icons: Launcher + Projekt — vertikal gestapelt */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1,
|
||||||
|
flexShrink: 0 }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => openDossierSettings()}
|
onClick={() => openDossierSettings()}
|
||||||
title="Dossier-Einstellungen (App-Settings, Window-Layout)"
|
title="Dossier-Einstellungen (App-Settings, Window-Layout)"
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent', border: 'none', padding: '2px 4px',
|
background: 'transparent', border: 'none', padding: '1px 4px',
|
||||||
cursor: 'pointer', color: 'var(--text-muted)',
|
cursor: 'pointer', color: 'var(--text-muted)',
|
||||||
display: 'flex', alignItems: 'center', flexShrink: 0,
|
display: 'flex', alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
|
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
|
||||||
>
|
>
|
||||||
<Icon name="settings" size={14} />
|
<Icon name="settings" size={12} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openProjectSettings()}
|
onClick={() => openProjectSettings()}
|
||||||
title="Projekt-Einstellungen (Voreinstellungen Geschoss/Schnitt + Material-Library)"
|
title="Projekt-Einstellungen (Voreinstellungen Geschoss/Schnitt + Material-Library)"
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent', border: 'none', padding: '2px 4px',
|
background: 'transparent', border: 'none', padding: '1px 4px',
|
||||||
cursor: 'pointer', color: 'var(--text-muted)',
|
cursor: 'pointer', color: 'var(--text-muted)',
|
||||||
display: 'flex', alignItems: 'center', flexShrink: 0,
|
display: 'flex', alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
|
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
|
||||||
>
|
>
|
||||||
<Icon name="tune" size={14} />
|
<Icon name="tune" size={12} />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<div style={sep} />
|
<div style={sep} />
|
||||||
{/* ====== VIEW 2x4 Grid ======
|
{/* ====== VIEW 2x4 Grid ======
|
||||||
Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings)
|
Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings)
|
||||||
|
|||||||
@@ -1,27 +1,67 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
|
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||||
|
|
||||||
/* MaterialCard — Preview-Swatch + Name + Tags + Import-Button. */
|
/* Preview — abhaengig vom Typ:
|
||||||
function MaterialCard({ item, imported, onImport }) {
|
material → Color-Swatch
|
||||||
const color = item.data?.color || '#888888'
|
symbol/object → Typ-Icon-Placeholder (spaeter: PNG-Thumbnail aus
|
||||||
|
library/previews/) */
|
||||||
|
function ItemPreview({ item }) {
|
||||||
|
if (item.type === 'material') {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: 64,
|
||||||
|
background: item.data?.color || '#888888',
|
||||||
|
borderBottom: '1px solid var(--border-light)',
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const iconName = item.type === 'symbol' ? 'navigation' : 'forest'
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: 64,
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
borderBottom: '1px solid var(--border-light)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Icon name={iconName} size={28}
|
||||||
|
style={{ color: 'var(--text-muted)' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ItemCard — Preview + Name + Tags + Import-Pill */
|
||||||
|
function ItemCard({ item, imported, onImport }) {
|
||||||
|
// material wird "importiert" gezeigt wenn schon in Project-Settings.
|
||||||
|
// symbol/object koennen N-mal eingefuegt werden → "Importiert" wird
|
||||||
|
// nicht persistent, jeder Klick legt eine weitere Instanz an.
|
||||||
|
const isMaterial = item.type === 'material'
|
||||||
|
const ctaLabel = isMaterial
|
||||||
|
? (imported ? 'Importiert' : 'Importieren')
|
||||||
|
: 'Einfügen'
|
||||||
|
const ctaDisabled = isMaterial && imported
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column',
|
display: 'flex', flexDirection: 'column',
|
||||||
border: '1px solid var(--border)', borderRadius: 6,
|
border: '1px solid var(--border)', borderRadius: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
cursor: imported ? 'default' : 'pointer',
|
cursor: ctaDisabled ? 'default' : 'pointer',
|
||||||
opacity: imported ? 0.75 : 1,
|
opacity: ctaDisabled ? 0.75 : 1,
|
||||||
|
transition: 'border-color 0.15s, background 0.15s',
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => { if (!imported) onImport(item.id) }}
|
onMouseEnter={(e) => {
|
||||||
title={imported ? 'Bereits importiert' : 'Doppelklick = importieren'}>
|
if (ctaDisabled) return
|
||||||
<div style={{
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||||
height: 64,
|
}}
|
||||||
background: color,
|
onMouseLeave={(e) => {
|
||||||
borderBottom: '1px solid var(--border-light)',
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
}} />
|
}}
|
||||||
<div style={{ padding: '6px 8px', display: 'flex',
|
onDoubleClick={() => { if (!ctaDisabled) onImport(item.id) }}
|
||||||
flexDirection: 'column', gap: 4 }}>
|
title={ctaDisabled ? 'Bereits importiert' : 'Doppelklick = importieren'}>
|
||||||
|
<ItemPreview item={item} />
|
||||||
|
<div style={{ padding: '8px 10px', display: 'flex',
|
||||||
|
flexDirection: 'column', gap: 6 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<span style={{ flex: 1, fontSize: 11, fontWeight: 600,
|
<span style={{ flex: 1, fontSize: 11, fontWeight: 600,
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
@@ -29,7 +69,7 @@ function MaterialCard({ item, imported, onImport }) {
|
|||||||
whiteSpace: 'nowrap' }}>
|
whiteSpace: 'nowrap' }}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
{imported && (
|
{ctaDisabled && (
|
||||||
<Icon name="check" size={12}
|
<Icon name="check" size={12}
|
||||||
style={{ color: 'var(--accent)' }} />
|
style={{ color: 'var(--accent)' }} />
|
||||||
)}
|
)}
|
||||||
@@ -40,23 +80,17 @@ function MaterialCard({ item, imported, onImport }) {
|
|||||||
<span key={t} style={{
|
<span key={t} style={{
|
||||||
fontSize: 9, color: 'var(--text-muted)',
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
background: 'var(--bg-input)',
|
background: 'var(--bg-input)',
|
||||||
padding: '1px 5px', borderRadius: 999,
|
padding: '1px 6px', borderRadius: 999,
|
||||||
|
border: '1px solid var(--border-light)',
|
||||||
}}>{t}</span>
|
}}>{t}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<BarToggle
|
||||||
onClick={() => { if (!imported) onImport(item.id) }}
|
label={ctaLabel}
|
||||||
disabled={imported}
|
active={!ctaDisabled}
|
||||||
style={{
|
onClick={() => { if (!ctaDisabled) onImport(item.id) }}
|
||||||
marginTop: 2, padding: '3px 6px', fontSize: 10,
|
disabled={ctaDisabled} />
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -97,30 +131,7 @@ export default function LibraryBrowser({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
||||||
}}>
|
}}>
|
||||||
{/* Header */}
|
{/* Toolbar — Suche + Pill-Filter + Reload */}
|
||||||
<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={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
@@ -129,24 +140,18 @@ export default function LibraryBrowser({
|
|||||||
<input type="text" value={search}
|
<input type="text" value={search}
|
||||||
onChange={(ev) => setSearch(ev.target.value)}
|
onChange={(ev) => setSearch(ev.target.value)}
|
||||||
placeholder="Suchen (Name oder Tag)…"
|
placeholder="Suchen (Name oder Tag)…"
|
||||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
|
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||||
borderRadius: 999 }} />
|
fontSize: 11 }} />
|
||||||
<div style={{ display: 'flex', gap: 0,
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 999, overflow: 'hidden' }}>
|
|
||||||
{types.map(t => (
|
{types.map(t => (
|
||||||
<button key={t}
|
<BarToggle key={t}
|
||||||
onClick={() => setTypeFilter(t)}
|
label={t === 'all' ? 'Alle' : t}
|
||||||
style={{
|
active={typeFilter === t}
|
||||||
background: typeFilter === t ? 'var(--accent)' : 'transparent',
|
onClick={() => setTypeFilter(t)} />
|
||||||
color: typeFilter === t ? '#fff' : 'var(--text-muted)',
|
|
||||||
border: 'none', padding: '4px 10px', fontSize: 10,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}>
|
|
||||||
{t === 'all' ? 'Alle' : t}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<BarButton icon="refresh" onClick={onReload}
|
||||||
|
title="Manifest neu laden" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
@@ -171,7 +176,7 @@ export default function LibraryBrowser({
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
}}>
|
}}>
|
||||||
{filtered.map(it => (
|
{filtered.map(it => (
|
||||||
<MaterialCard key={it.id}
|
<ItemCard key={it.id}
|
||||||
item={it}
|
item={it}
|
||||||
imported={importedSet.has(it.id)}
|
imported={importedSet.has(it.id)}
|
||||||
onImport={onImport} />
|
onImport={onImport} />
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
|
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||||
import { openLibrary } from '../lib/rhinoBridge'
|
import { openLibrary } from '../lib/rhinoBridge'
|
||||||
|
|
||||||
/* Inline Field + Tab helpers fuer kompaktes Layout im Satellite. */
|
/* Pill-Stil Field — Label klein in Caps, Inhalt darunter, optional Hint */
|
||||||
function Field({ label, hint, children, style }) {
|
function Field({ label, hint, children, style }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3,
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4,
|
||||||
padding: '5px 0', ...style }}>
|
padding: '6px 0', ...style }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-secondary)',
|
<span style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
fontWeight: 500, letterSpacing: 0.2 }}>{label}</span>
|
fontWeight: 500, letterSpacing: '0.06em',
|
||||||
|
textTransform: 'uppercase' }}>{label}</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
|
||||||
{hint && (
|
{hint && (
|
||||||
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
||||||
@@ -19,56 +21,54 @@ function Field({ label, hint, children, style }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pill-Tabs — gleicher Stil wie BarToggle aus der Oberleiste */
|
||||||
function TabBar({ tabs, active, onChange }) {
|
function TabBar({ tabs, active, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', gap: 0,
|
display: 'flex', gap: 4,
|
||||||
|
padding: '8px 12px',
|
||||||
borderBottom: '1px solid var(--border)',
|
borderBottom: '1px solid var(--border)',
|
||||||
paddingLeft: 4,
|
|
||||||
}}>
|
}}>
|
||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<button key={t.key}
|
<BarToggle key={t.key}
|
||||||
onClick={() => onChange(t.key)}
|
label={t.label}
|
||||||
style={{
|
active={active === t.key}
|
||||||
background: 'transparent',
|
onClick={() => onChange(t.key)} />
|
||||||
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
||||||
|
const isBuiltin = builtin || mat.source === 'builtin'
|
||||||
|
const isLibrary = mat.source === 'library'
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid', gridTemplateColumns: '14px 1fr 90px 60px 18px',
|
display: 'grid', gridTemplateColumns: '18px 1fr 92px 56px 22px',
|
||||||
alignItems: 'center', gap: 6,
|
alignItems: 'center', gap: 6,
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderBottom: '1px solid var(--border-light)',
|
borderRadius: 6,
|
||||||
background: builtin ? 'var(--bg-section)' : 'transparent',
|
background: isBuiltin ? 'var(--bg-section)' : 'transparent',
|
||||||
}}>
|
transition: 'background 0.12s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'var(--bg-item-hover)' }}
|
||||||
|
onMouseLeave={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
<input type="color" value={mat.color || '#888888'}
|
<input type="color" value={mat.color || '#888888'}
|
||||||
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
||||||
title="Farbe"
|
title="Farbe"
|
||||||
style={{ width: 14, height: 14, padding: 0, border: 'none',
|
style={{ width: 18, height: 18, padding: 0, border: 'none',
|
||||||
background: 'transparent', cursor: 'pointer' }} />
|
background: 'transparent', cursor: 'pointer' }} />
|
||||||
<input type="text" value={mat.name || ''}
|
<input type="text" value={mat.name || ''}
|
||||||
onChange={(ev) => onChange({ ...mat, name: ev.target.value })}
|
onChange={(ev) => onChange({ ...mat, name: ev.target.value })}
|
||||||
disabled={builtin}
|
disabled={isBuiltin}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
style={{ flex: 1, fontSize: 11, minWidth: 0,
|
style={{ minWidth: 0, height: BAR_H, padding: '0 10px',
|
||||||
opacity: builtin ? 0.7 : 1 }} />
|
fontSize: 11,
|
||||||
|
opacity: isBuiltin ? 0.7 : 1 }} />
|
||||||
<select value={mat.hatch || 'Solid'}
|
<select value={mat.hatch || 'Solid'}
|
||||||
onChange={(ev) => onChange({ ...mat, hatch: ev.target.value })}
|
onChange={(ev) => onChange({ ...mat, hatch: ev.target.value })}
|
||||||
style={{ fontSize: 11, minWidth: 0 }}>
|
style={{ minWidth: 0, height: BAR_H, fontSize: 10 }}>
|
||||||
{(hatchPatterns || []).map(h => (
|
{(hatchPatterns || []).map(h => (
|
||||||
<option key={h} value={h}>{h}</option>
|
<option key={h} value={h}>{h}</option>
|
||||||
))}
|
))}
|
||||||
@@ -77,19 +77,30 @@ function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
|||||||
value={mat.scale ?? 1.0}
|
value={mat.scale ?? 1.0}
|
||||||
onChange={(ev) => onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })}
|
onChange={(ev) => onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })}
|
||||||
title="Hatch-Skalierung"
|
title="Hatch-Skalierung"
|
||||||
style={{ fontSize: 11, textAlign: 'right' }} />
|
style={{ minWidth: 0, height: BAR_H, padding: '0 10px',
|
||||||
{mat.source === 'library' ? (
|
fontSize: 11, textAlign: 'right' }} />
|
||||||
<span style={{ fontSize: 9, color: 'var(--accent-light)' }}
|
{isLibrary ? (
|
||||||
|
<span style={{ fontSize: 9, fontWeight: 600,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
display: 'inline-flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center' }}
|
||||||
title="Aus Dossier-Library">L</span>
|
title="Aus Dossier-Library">L</span>
|
||||||
) : builtin || mat.source === 'builtin' ? (
|
) : isBuiltin ? (
|
||||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}
|
<span style={{ fontSize: 9, fontWeight: 600,
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
display: 'inline-flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center' }}
|
||||||
title="Eingebaut">B</span>
|
title="Eingebaut">B</span>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={onDelete} title="Loeschen"
|
<button onClick={onDelete} title="Loeschen"
|
||||||
style={{ background: 'transparent', border: 'none',
|
style={{ background: 'transparent', border: 'none',
|
||||||
padding: 0, cursor: 'pointer',
|
padding: 0, cursor: 'pointer',
|
||||||
color: 'var(--text-muted)' }}>
|
color: 'var(--text-muted)',
|
||||||
<Icon name="close" size={12} />
|
display: 'inline-flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center' }}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--accent)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}>
|
||||||
|
<Icon name="close" size={14} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -121,13 +132,15 @@ export default function ProjectSettingsDialog({
|
|||||||
materials: [...d.materials, {
|
materials: [...d.materials, {
|
||||||
name: 'Neues Material', color: '#aaaaaa',
|
name: 'Neues Material', color: '#aaaaaa',
|
||||||
hatch: 'Solid', scale: 1.0,
|
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,
|
source: 'local', libraryId: null,
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const numberInputStyle = {
|
||||||
|
flex: 1, height: BAR_H, padding: '0 12px',
|
||||||
|
fontSize: 11, textAlign: 'right',
|
||||||
|
}
|
||||||
|
|
||||||
const wrapperStyle = embedded ? {
|
const wrapperStyle = embedded ? {
|
||||||
width: '100%', height: '100%',
|
width: '100%', height: '100%',
|
||||||
background: 'var(--bg-dialog)',
|
background: 'var(--bg-dialog)',
|
||||||
@@ -144,22 +157,6 @@ export default function ProjectSettingsDialog({
|
|||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
||||||
minHeight: 0, overflow: 'hidden' }}>
|
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={[
|
<TabBar tabs={[
|
||||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||||
{ key: 'materials', label: 'Materialien' },
|
{ key: 'materials', label: 'Materialien' },
|
||||||
@@ -170,47 +167,46 @@ export default function ProjectSettingsDialog({
|
|||||||
padding: '8px 14px' }}>
|
padding: '8px 14px' }}>
|
||||||
{tab === 'defaults' && (
|
{tab === 'defaults' && (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||||
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
||||||
Diese Werte werden beim Erstellen neuer Elemente als
|
Voreinstellungen fuer neue Elemente. Pro-Element editierte
|
||||||
Voreinstellung genommen. Pro-Element editierte Werte
|
Werte bleiben davon unberuehrt.
|
||||||
bleiben davon unberuehrt.
|
|
||||||
</div>
|
</div>
|
||||||
<Field label="STANDARD-GESCHOSSHÖHE (m)"
|
<Field label="Standard-Geschosshöhe (m)"
|
||||||
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
|
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
|
||||||
<input type="number" step="0.05" min="1.0" max="10"
|
<input type="number" step="0.05" min="1.0" max="10"
|
||||||
value={draft.defaults.geschossHoehe ?? 3.0}
|
value={draft.defaults.geschossHoehe ?? 3.0}
|
||||||
onChange={(ev) => setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)}
|
onChange={(ev) => setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)}
|
||||||
style={{ flex: 1, textAlign: 'right' }} />
|
style={numberInputStyle} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="STANDARD-SCHNITTHÖHE (m)"
|
<Field label="Standard-Schnitthöhe (m)"
|
||||||
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses">
|
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses">
|
||||||
<input type="number" step="0.05" min="0.1" max="3"
|
<input type="number" step="0.05" min="0.1" max="3"
|
||||||
value={draft.defaults.schnitthoehe ?? 1.0}
|
value={draft.defaults.schnitthoehe ?? 1.0}
|
||||||
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
||||||
style={{ flex: 1, textAlign: 'right' }} />
|
style={numberInputStyle} />
|
||||||
</Field>
|
</Field>
|
||||||
<div style={{ height: 1, background: 'var(--border-light)',
|
<div style={{ height: 1, background: 'var(--border-light)',
|
||||||
margin: '8px 0' }} />
|
margin: '12px 0' }} />
|
||||||
<Field label="STANDARD-SCHNITT-TIEFE HINTEN (m)"
|
<Field label="Standard-Schnitt-Tiefe hinten (m)"
|
||||||
hint="Default-Tiefe für neue Schnitte/Ansichten — wie weit hinter der Schnittlinie sichtbar">
|
hint="Default-Tiefe fuer neue Schnitte/Ansichten">
|
||||||
<input type="number" step="0.5" min="0.5"
|
<input type="number" step="0.5" min="0.5"
|
||||||
value={draft.defaults.schnittDepthBack ?? 8.0}
|
value={draft.defaults.schnittDepthBack ?? 8.0}
|
||||||
onChange={(ev) => setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)}
|
onChange={(ev) => setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)}
|
||||||
style={{ flex: 1, textAlign: 'right' }} />
|
style={numberInputStyle} />
|
||||||
</Field>
|
</Field>
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Field label="HÖHE UNTEN (m)" style={{ flex: 1 }}>
|
<Field label="Höhe unten (m)" style={{ flex: 1 }}>
|
||||||
<input type="number" step="0.1"
|
<input type="number" step="0.1"
|
||||||
value={draft.defaults.schnittHeightMin ?? -1.0}
|
value={draft.defaults.schnittHeightMin ?? -1.0}
|
||||||
onChange={(ev) => setDefault('schnittHeightMin', parseFloat(ev.target.value))}
|
onChange={(ev) => setDefault('schnittHeightMin', parseFloat(ev.target.value))}
|
||||||
style={{ flex: 1, textAlign: 'right' }} />
|
style={numberInputStyle} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="HÖHE OBEN (m)" style={{ flex: 1 }}>
|
<Field label="Höhe oben (m)" style={{ flex: 1 }}>
|
||||||
<input type="number" step="0.1"
|
<input type="number" step="0.1"
|
||||||
value={draft.defaults.schnittHeightMax ?? 12.0}
|
value={draft.defaults.schnittHeightMax ?? 12.0}
|
||||||
onChange={(ev) => setDefault('schnittHeightMax', parseFloat(ev.target.value))}
|
onChange={(ev) => setDefault('schnittHeightMax', parseFloat(ev.target.value))}
|
||||||
style={{ flex: 1, textAlign: 'right' }} />
|
style={numberInputStyle} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -218,21 +214,19 @@ export default function ProjectSettingsDialog({
|
|||||||
|
|
||||||
{tab === 'materials' && (
|
{tab === 'materials' && (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||||
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
||||||
Eingebaute Materialien (B) koennen nicht umbenannt werden,
|
Eingebaute Materialien (B) — Farbe + Hatch anpassbar, Name fix.
|
||||||
aber Farbe + Hatch sind anpassbar. Eigene Materialien
|
Library-Materialien (L) aus Dossier-Library importiert.
|
||||||
koennen frei angelegt werden.
|
Lokale Materialien frei editierbar.
|
||||||
</div>
|
</div>
|
||||||
{/* Built-in materials (read-only name) */}
|
{builtin.map((m) => (
|
||||||
{builtin.map((m, i) => (
|
|
||||||
<MaterialRow key={'b_' + m.name}
|
<MaterialRow key={'b_' + m.name}
|
||||||
mat={m}
|
mat={m}
|
||||||
builtin
|
builtin
|
||||||
hatchPatterns={hatchPatterns}
|
hatchPatterns={hatchPatterns}
|
||||||
onChange={() => {/* read-only fuer Phase 1 */}} />
|
onChange={() => {/* read-only Phase 1 */}} />
|
||||||
))}
|
))}
|
||||||
{/* User materials */}
|
|
||||||
{draft.materials.map((m, i) => (
|
{draft.materials.map((m, i) => (
|
||||||
<MaterialRow key={'u_' + i}
|
<MaterialRow key={'u_' + i}
|
||||||
mat={m}
|
mat={m}
|
||||||
@@ -240,36 +234,18 @@ export default function ProjectSettingsDialog({
|
|||||||
onChange={(nm) => setMat(i, nm)}
|
onChange={(nm) => setMat(i, nm)}
|
||||||
onDelete={() => delMat(i)} />
|
onDelete={() => delMat(i)} />
|
||||||
))}
|
))}
|
||||||
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
|
<div style={{ display: 'flex', gap: 6, marginTop: 12 }}>
|
||||||
<button onClick={addMat}
|
<BarToggle icon="add" label="Material" onClick={addMat}
|
||||||
style={{
|
title="Neues Material anlegen" />
|
||||||
padding: '4px 10px', fontSize: 11,
|
<BarToggle icon="inventory_2" label="Library"
|
||||||
background: 'var(--bg-input)',
|
onClick={openLibrary}
|
||||||
border: '1px solid var(--border)',
|
title="Material aus Dossier-Library importieren" />
|
||||||
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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer — Pill-Buttons */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
@@ -277,10 +253,8 @@ export default function ProjectSettingsDialog({
|
|||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
<BarToggle label="Abbrechen" onClick={onClose} />
|
||||||
<button className="btn-contained" onClick={() => onSave(draft)}>
|
<BarToggle label="Übernehmen" active onClick={() => onSave(draft)} />
|
||||||
Übernehmen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-3
@@ -189,11 +189,13 @@ input[type="range"]:hover {
|
|||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pill-shaped select */
|
/* Pill-shaped select — gleicher Hintergrund wie input für konsistentes
|
||||||
|
Oberleiste-Pill-Look (in dark mode = dunkler, light mode = leicht
|
||||||
|
gehoben). Vorher --bg-item wirkte im dark mode "hell" gegen Inputs. */
|
||||||
select {
|
select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: var(--bg-item);
|
background-color: var(--bg-input);
|
||||||
background-image: var(--select-arrow);
|
background-image: var(--select-arrow);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 10px center;
|
background-position: right 10px center;
|
||||||
@@ -209,7 +211,7 @@ select {
|
|||||||
}
|
}
|
||||||
select:hover {
|
select:hover {
|
||||||
background-color: var(--bg-item-hover);
|
background-color: var(--bg-item-hover);
|
||||||
border-color: var(--text-muted);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Opt-out: System-native Chrome (Oberleiste). Setzt die globalen
|
/* Opt-out: System-native Chrome (Oberleiste). Setzt die globalen
|
||||||
|
|||||||
Reference in New Issue
Block a user