Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c63bdd5bc1 | |||
| 0bf891641f |
+145
-12
@@ -2208,10 +2208,19 @@ _MATERIAL_LIBRARY = {
|
|||||||
|
|
||||||
def _get_all_materials(doc):
|
def _get_all_materials(doc):
|
||||||
"""Builtin _MATERIAL_LIBRARY + Projekt-Settings-Materialien gemerged.
|
"""Builtin _MATERIAL_LIBRARY + Projekt-Settings-Materialien gemerged.
|
||||||
Returns dict[name] -> {color, hatch, scale}. Projekt-Settings (inkl.
|
Returns dict[name] -> full material dict (color/hatch/scale + PBR +
|
||||||
Library-Imports) ueberschreibt builtin bei Namensgleichheit, sodass
|
textures + uvScaleM). Projekt-Settings ueberschreibt builtin bei
|
||||||
der User builtin-Defaults pro Projekt anpassen kann."""
|
Namensgleichheit. Builtin-Materialien bekommen leere PBR-Defaults."""
|
||||||
merged = {n: dict(m) for n, m in _MATERIAL_LIBRARY.items()}
|
merged = {}
|
||||||
|
for n, m in _MATERIAL_LIBRARY.items():
|
||||||
|
merged[n] = dict(m)
|
||||||
|
# Builtin: PBR-Defaults wenn nicht gesetzt
|
||||||
|
merged[n].setdefault("roughness", 0.7)
|
||||||
|
merged[n].setdefault("reflection", 0.1)
|
||||||
|
merged[n].setdefault("transparency", 0.0)
|
||||||
|
merged[n].setdefault("iorN", 1.0)
|
||||||
|
merged[n].setdefault("uvScaleM", 1.0)
|
||||||
|
merged[n].setdefault("textures", {})
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import rhinopanel
|
||||||
ps = rhinopanel.load_project_settings(doc) if doc else None
|
ps = rhinopanel.load_project_settings(doc) if doc else None
|
||||||
@@ -2219,11 +2228,9 @@ def _get_all_materials(doc):
|
|||||||
for m in ps.get("materials", []):
|
for m in ps.get("materials", []):
|
||||||
n = m.get("name")
|
n = m.get("name")
|
||||||
if not n: continue
|
if not n: continue
|
||||||
merged[n] = {
|
# Komplettes dict uebernehmen — _normalize_material hat
|
||||||
"color": m.get("color", "#888888"),
|
# bereits alle Felder validiert.
|
||||||
"hatch": m.get("hatch", "Solid"),
|
merged[n] = dict(m)
|
||||||
"scale": float(m.get("scale", 1.0) or 1.0),
|
|
||||||
}
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] _get_all_materials:", ex)
|
print("[ELEMENTE] _get_all_materials:", ex)
|
||||||
return merged
|
return merged
|
||||||
@@ -2330,6 +2337,126 @@ def _ensure_material(doc, hex_color):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_pbr_material(doc, mat_dict):
|
||||||
|
"""Erstellt oder findet ein Rhino-PBR-Material aus dem Project-Settings-
|
||||||
|
dict (color + roughness + reflection + transparency + iorN + textures +
|
||||||
|
uvScaleM). Cached pro signature in sc.sticky. Liefert Material-Index
|
||||||
|
oder -1.
|
||||||
|
Setzt:
|
||||||
|
- PhysicallyBased.BaseColor (aus color)
|
||||||
|
- PhysicallyBased.Roughness, Opacity (= 1-transparency), OpacityIor
|
||||||
|
- PhysicallyBased.Metallic = reflection (Naeherung)
|
||||||
|
- SetBitmapTexture / SetBumpTexture / SetTransparencyTexture wenn
|
||||||
|
Pfade gesetzt
|
||||||
|
- Texture.Repeat = (1/uvScaleM, 1/uvScaleM)"""
|
||||||
|
if not isinstance(mat_dict, dict): return -1
|
||||||
|
color = mat_dict.get("color") or "#888888"
|
||||||
|
if not isinstance(color, str) or not color.startswith("#") or len(color) < 7:
|
||||||
|
return -1
|
||||||
|
# Cache-Key: alle PBR-Felder + Texture-Pfade
|
||||||
|
texs = mat_dict.get("textures") or {}
|
||||||
|
def _p(slot):
|
||||||
|
t = texs.get(slot) if isinstance(texs, dict) else None
|
||||||
|
return (t or {}).get("path", "") if isinstance(t, dict) else ""
|
||||||
|
sig = "|".join([
|
||||||
|
color.lower(),
|
||||||
|
"r{:.3f}".format(float(mat_dict.get("roughness", 0.7))),
|
||||||
|
"x{:.3f}".format(float(mat_dict.get("reflection", 0.1))),
|
||||||
|
"t{:.3f}".format(float(mat_dict.get("transparency", 0.0))),
|
||||||
|
"i{:.3f}".format(float(mat_dict.get("iorN", 1.0))),
|
||||||
|
"u{:.3f}".format(float(mat_dict.get("uvScaleM", 1.0))),
|
||||||
|
"d=" + _p("diffuse"),
|
||||||
|
"b=" + _p("bump"),
|
||||||
|
"g=" + _p("roughness"),
|
||||||
|
"p=" + _p("transparency"),
|
||||||
|
])
|
||||||
|
cache = sc.sticky.get("_dossier_pbr_material_cache")
|
||||||
|
if not isinstance(cache, dict):
|
||||||
|
cache = {}
|
||||||
|
sc.sticky["_dossier_pbr_material_cache"] = cache
|
||||||
|
cached = cache.get(sig)
|
||||||
|
if cached is not None:
|
||||||
|
try:
|
||||||
|
if 0 <= cached < doc.Materials.Count:
|
||||||
|
m = doc.Materials[cached]
|
||||||
|
if m is not None and not m.IsDeleted:
|
||||||
|
return cached
|
||||||
|
except Exception: pass
|
||||||
|
del cache[sig]
|
||||||
|
# Material bauen
|
||||||
|
try:
|
||||||
|
import System.Drawing as SD
|
||||||
|
h = color.lstrip("#")
|
||||||
|
r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16)
|
||||||
|
mat = Rhino.DocObjects.Material()
|
||||||
|
mat.Name = "Dossier_PBR_" + (mat_dict.get("name") or "Mat")[:20]
|
||||||
|
mat.DiffuseColor = SD.Color.FromArgb(255, r, g, b)
|
||||||
|
# PBR-Properties
|
||||||
|
try:
|
||||||
|
mat.ToPhysicallyBased()
|
||||||
|
pbr = mat.PhysicallyBased
|
||||||
|
if pbr is not None:
|
||||||
|
# Color4f (linear) — RGB normalisieren
|
||||||
|
try:
|
||||||
|
from Rhino.Display import Color4f
|
||||||
|
pbr.BaseColor = Color4f(r/255.0, g/255.0, b/255.0, 1.0)
|
||||||
|
except Exception: pass
|
||||||
|
try: pbr.Roughness = float(mat_dict.get("roughness", 0.7))
|
||||||
|
except Exception: pass
|
||||||
|
# reflection ~ metallic (Naeherung — bei nicht-Metallen
|
||||||
|
# bleibt Roughness/IoR das Hauptmerkmal). User kann das
|
||||||
|
# spaeter via separates Metallic-Feld feiner steuern.
|
||||||
|
try: pbr.Metallic = float(mat_dict.get("reflection", 0.0))
|
||||||
|
except Exception: pass
|
||||||
|
trans = float(mat_dict.get("transparency", 0.0))
|
||||||
|
try: pbr.Opacity = max(0.0, 1.0 - trans)
|
||||||
|
except Exception: pass
|
||||||
|
try: pbr.OpacityIor = float(mat_dict.get("iorN", 1.0))
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] PBR setup:", ex)
|
||||||
|
# Texturen + UV-Repeat
|
||||||
|
try:
|
||||||
|
uv = float(mat_dict.get("uvScaleM", 1.0)) or 1.0
|
||||||
|
uv_repeat = 1.0 / uv # uvScaleM = 1m → 1x; 0.5m → 2x Repeat
|
||||||
|
except Exception: uv_repeat = 1.0
|
||||||
|
def _apply_tex(slot, setter_name):
|
||||||
|
p = _p(slot)
|
||||||
|
if not p: return
|
||||||
|
try:
|
||||||
|
setter = getattr(mat, setter_name, None)
|
||||||
|
if setter is None: return
|
||||||
|
ok = setter(p)
|
||||||
|
if not ok: return
|
||||||
|
# Repeat auf den frisch gesetzten Texture-Slot — alle
|
||||||
|
# Slot-Indices durchgehen und matchen.
|
||||||
|
try:
|
||||||
|
import Rhino.Geometry as rgg
|
||||||
|
for i in range(mat.GetTextures().Count if False else 100):
|
||||||
|
try: t = mat.GetTexture(i)
|
||||||
|
except Exception: break
|
||||||
|
if t is None: continue
|
||||||
|
try: t.Repeat = rgg.Vector2d(uv_repeat, uv_repeat)
|
||||||
|
except Exception: pass
|
||||||
|
try: mat.SetTexture(t, t.TextureType)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] apply tex {}:".format(slot), ex)
|
||||||
|
_apply_tex("diffuse", "SetBitmapTexture")
|
||||||
|
_apply_tex("bump", "SetBumpTexture")
|
||||||
|
_apply_tex("transparency", "SetTransparencyTexture")
|
||||||
|
# Roughness-Texture: kein direkter Setter im Legacy-Material —
|
||||||
|
# PhysicallyBased haette eine, ueberspringen wir hier (Phase B+)
|
||||||
|
idx = doc.Materials.Add(mat)
|
||||||
|
if idx >= 0:
|
||||||
|
cache[sig] = idx
|
||||||
|
return idx
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] _ensure_pbr_material:", ex)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def _make_wand_layer_breps(axis_curve, layers, dicke, referenz, uk, ok,
|
def _make_wand_layer_breps(axis_curve, layers, dicke, referenz, uk, ok,
|
||||||
miter_start=None, miter_end=None):
|
miter_start=None, miter_end=None):
|
||||||
"""Baut eine Liste (brep, color_hex, name) pro Schicht. Schicht-Reihen-
|
"""Baut eine Liste (brep, color_hex, name) pro Schicht. Schicht-Reihen-
|
||||||
@@ -5451,8 +5578,10 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
|||||||
effective_color = color
|
effective_color = color
|
||||||
target_layer = layer
|
target_layer = layer
|
||||||
all_mats = _get_all_materials(doc)
|
all_mats = _get_all_materials(doc)
|
||||||
|
full_mat_dict = None # PBR + Texturen wenn aus Project-Settings
|
||||||
if mat_name and mat_name in all_mats:
|
if mat_name and mat_name in all_mats:
|
||||||
effective_color = all_mats[mat_name]["color"]
|
full_mat_dict = all_mats[mat_name]
|
||||||
|
effective_color = full_mat_dict.get("color", color)
|
||||||
target_layer = _ensure_material_sublayer(doc, geschoss_name,
|
target_layer = _ensure_material_sublayer(doc, geschoss_name,
|
||||||
mat_name)
|
mat_name)
|
||||||
if target_layer < 0: target_layer = layer
|
if target_layer < 0: target_layer = layer
|
||||||
@@ -5467,8 +5596,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
|||||||
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
|
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
|
||||||
attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0)
|
attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
# Faces via Material (DiffuseColor) — getrennt vom ObjectColor.
|
# Faces via Material — wenn voll-dict mit PBR vorhanden, PBR-
|
||||||
if effective_color:
|
# Material mit Texturen bauen, sonst einfaches Diffuse-Material.
|
||||||
|
mat_idx = -1
|
||||||
|
if full_mat_dict is not None:
|
||||||
|
mat_idx = _ensure_pbr_material(doc, full_mat_dict)
|
||||||
|
if mat_idx < 0 and effective_color:
|
||||||
mat_idx = _ensure_material(doc, effective_color)
|
mat_idx = _ensure_material(doc, effective_color)
|
||||||
if mat_idx >= 0:
|
if mat_idx >= 0:
|
||||||
attrs.MaterialIndex = mat_idx
|
attrs.MaterialIndex = mat_idx
|
||||||
|
|||||||
+119
-5
@@ -141,21 +141,62 @@ _PROJECT_SETTINGS_DEFAULTS = {
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_material(m):
|
def _normalize_material(m):
|
||||||
"""Garantiert Material-Schema: name + color + hatch + scale + source
|
"""Garantiert Material-Schema. Felder:
|
||||||
+ libraryId. source: 'local' | 'library' | 'builtin'. libraryId: UUID
|
- Identitaet: name, source ('local'|'library'|'builtin'), libraryId
|
||||||
wenn aus Library, sonst null. Felder fehlen bei alten Eintraegen — wir
|
- Section-Hatch (2D): color, hatch, scale
|
||||||
setzen Defaults damit Frontend nicht null-checken muss."""
|
- PBR (3D-Render): roughness (0..1), reflection (0..1),
|
||||||
|
transparency (0..1), iorN (1.0..2.5)
|
||||||
|
- UV: uvScaleM (= 1 Welt-Meter ≙ wieviel der Textur)
|
||||||
|
- Texturen: textures = { diffuse, bump, roughness, transparency }
|
||||||
|
pro Slot {path: absolute string} oder null. Strength fuer Bump."""
|
||||||
if not isinstance(m, dict): return None
|
if not isinstance(m, dict): return None
|
||||||
|
textures = m.get("textures") or {}
|
||||||
|
if not isinstance(textures, dict): textures = {}
|
||||||
|
def _tex(slot):
|
||||||
|
t = textures.get(slot)
|
||||||
|
if not isinstance(t, dict): return None
|
||||||
|
p = t.get("path")
|
||||||
|
if not p: return None
|
||||||
|
out = {"path": str(p)}
|
||||||
|
# Bump hat zusaetzlich strength (-1..1, default 0.5)
|
||||||
|
if slot == "bump":
|
||||||
|
try: out["strength"] = float(t.get("strength", 0.5))
|
||||||
|
except Exception: out["strength"] = 0.5
|
||||||
|
return out
|
||||||
return {
|
return {
|
||||||
"name": m.get("name") or "Unbenannt",
|
"name": m.get("name") or "Unbenannt",
|
||||||
"color": m.get("color") or "#888888",
|
"color": m.get("color") or "#888888",
|
||||||
"hatch": m.get("hatch") or "Solid",
|
"hatch": m.get("hatch") or "Solid",
|
||||||
"scale": float(m.get("scale", 1.0) or 1.0),
|
"scale": float(m.get("scale", 1.0) or 1.0),
|
||||||
"source": m.get("source") or "local",
|
"source": m.get("source") or "local",
|
||||||
"libraryId": m.get("libraryId"), # None wenn nicht aus Library
|
"libraryId": m.get("libraryId"),
|
||||||
|
# PBR (3D-Render) — alle 0..1 ausser iorN
|
||||||
|
"roughness": _clamp01(m.get("roughness", 0.7)),
|
||||||
|
"reflection": _clamp01(m.get("reflection", 0.1)),
|
||||||
|
"transparency": _clamp01(m.get("transparency", 0.0)),
|
||||||
|
"iorN": _clamp(m.get("iorN", 1.0), 1.0, 2.5),
|
||||||
|
# UV-Skalierung (1 m = uvScaleM Textur-Tiles)
|
||||||
|
"uvScaleM": float(m.get("uvScaleM", 1.0) or 1.0),
|
||||||
|
# Texturen
|
||||||
|
"textures": {
|
||||||
|
"diffuse": _tex("diffuse"),
|
||||||
|
"bump": _tex("bump"),
|
||||||
|
"roughness": _tex("roughness"),
|
||||||
|
"transparency": _tex("transparency"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp01(v):
|
||||||
|
try: return max(0.0, min(1.0, float(v)))
|
||||||
|
except Exception: return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp(v, lo, hi):
|
||||||
|
try: return max(lo, min(hi, float(v)))
|
||||||
|
except Exception: return lo
|
||||||
|
|
||||||
|
|
||||||
def load_project_settings(doc):
|
def load_project_settings(doc):
|
||||||
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn
|
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn
|
||||||
Felder fehlen. Garantiert dass `defaults` und `materials` immer da
|
Felder fehlen. Garantiert dass `defaults` und `materials` immer da
|
||||||
@@ -617,6 +658,12 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
try: self._open_library()
|
try: self._open_library()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] open library:", ex)
|
print("[EBENEN] open library:", ex)
|
||||||
|
elif t == "PICK_TEXTURE_FILE":
|
||||||
|
# Oeffnet macOS-File-Picker fuer Bild-Dateien. Antwort an
|
||||||
|
# Frontend via TEXTURE_PICKED-Message.
|
||||||
|
try: self._pick_texture_file(p)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] pick texture:", ex)
|
||||||
|
|
||||||
# ---- Helpers ----
|
# ---- Helpers ----
|
||||||
|
|
||||||
@@ -659,12 +706,49 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
}
|
}
|
||||||
save_project_settings(doc2, new_settings)
|
save_project_settings(doc2, new_settings)
|
||||||
_broadcast_state(doc2)
|
_broadcast_state(doc2)
|
||||||
|
# Material-Cache invalidieren (PBR-Cache hashed Color+Texturen+
|
||||||
|
# PBR-Werte — wenn der User ein Material aendert, muss der
|
||||||
|
# Cache leer sein, sonst kriegen Waende stale Material-Indizes).
|
||||||
|
try:
|
||||||
|
import scriptcontext as sc
|
||||||
|
sc.sticky["_dossier_pbr_material_cache"] = {}
|
||||||
|
sc.sticky["_dossier_material_cache"] = {}
|
||||||
|
except Exception: pass
|
||||||
try:
|
try:
|
||||||
import scriptcontext as sc
|
import scriptcontext as sc
|
||||||
eb = sc.sticky.get("elemente_bridge")
|
eb = sc.sticky.get("elemente_bridge")
|
||||||
if eb is not None: eb._send_state()
|
if eb is not None: eb._send_state()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[PROJECT-SETTINGS] elemente refresh:", ex)
|
print("[PROJECT-SETTINGS] elemente refresh:", ex)
|
||||||
|
# Alle wand_axis im Doc regenen damit Material-Aenderungen
|
||||||
|
# (PBR/Texturen/Hatch) auf existing Waende durchschlagen.
|
||||||
|
try:
|
||||||
|
import elemente
|
||||||
|
undo_serial = doc2.BeginUndoRecord("Material-Update Regen")
|
||||||
|
prev_redraw = doc2.Views.RedrawEnabled
|
||||||
|
doc2.Views.RedrawEnabled = False
|
||||||
|
wall_ids = []
|
||||||
|
for obj in doc2.Objects:
|
||||||
|
m = elemente._read_meta(obj)
|
||||||
|
if m and m.get("type") == "wand_axis":
|
||||||
|
wall_ids.append(m["id"])
|
||||||
|
# Chain-Anchor regent automatisch alle members — wir koennen
|
||||||
|
# trotzdem alle einzeln triggern, _REGEN_BUSY-Guard verhindert
|
||||||
|
# Doppel-Arbeit. Einfacher als Anchor-Election hier.
|
||||||
|
try:
|
||||||
|
for wid in wall_ids:
|
||||||
|
try: elemente._regenerate_element(doc2, wid)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PROJECT-SETTINGS] regen", wid, ex)
|
||||||
|
finally:
|
||||||
|
doc2.Views.RedrawEnabled = prev_redraw
|
||||||
|
try: doc2.EndUndoRecord(undo_serial)
|
||||||
|
except Exception: pass
|
||||||
|
try: doc2.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
print("[PROJECT-SETTINGS] {} Waende regenert".format(len(wall_ids)))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PROJECT-SETTINGS] wall regen:", ex)
|
||||||
panel_base.open_satellite_window(
|
panel_base.open_satellite_window(
|
||||||
"project_settings",
|
"project_settings",
|
||||||
params=params,
|
params=params,
|
||||||
@@ -672,6 +756,36 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
size=(440, 540),
|
size=(440, 540),
|
||||||
on_save=on_save)
|
on_save=on_save)
|
||||||
|
|
||||||
|
def _pick_texture_file(self, payload):
|
||||||
|
"""Oeffnet macOS-File-Picker (via Eto.Forms.OpenFileDialog) und
|
||||||
|
schickt den ausgewaehlten Pfad zurueck ans Frontend.
|
||||||
|
Payload: {slot: 'diffuse'|'bump'|...} — slot wird mit zurueckgegeben
|
||||||
|
damit das Frontend weiss welches Slot-Field aktualisieren."""
|
||||||
|
slot = payload.get("slot") or "diffuse"
|
||||||
|
try:
|
||||||
|
import Eto.Forms as forms
|
||||||
|
dlg = forms.OpenFileDialog()
|
||||||
|
dlg.Title = "Textur waehlen ({})".format(slot)
|
||||||
|
dlg.MultiSelect = False
|
||||||
|
f = forms.FileFilter("Bilder",
|
||||||
|
".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".tga")
|
||||||
|
dlg.Filters.Add(f)
|
||||||
|
dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
|
||||||
|
try:
|
||||||
|
parent_form = sc.sticky.get("_dossier_active_settings_form")
|
||||||
|
except Exception:
|
||||||
|
parent_form = None
|
||||||
|
res = (dlg.ShowDialog(parent_form) if parent_form
|
||||||
|
else dlg.ShowDialog(None))
|
||||||
|
if str(res) != "Ok":
|
||||||
|
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
|
||||||
|
return
|
||||||
|
path = dlg.FileName or ""
|
||||||
|
self.send("TEXTURE_PICKED", {"slot": slot, "path": path})
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] pick texture:", ex)
|
||||||
|
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
|
||||||
|
|
||||||
def _open_library(self):
|
def _open_library(self):
|
||||||
"""Oeffnet den Library-Browser als Satellite. Bridge bleibt offen
|
"""Oeffnet den Library-Browser als Satellite. Bridge bleibt offen
|
||||||
damit User mehrere Items hintereinander importieren kann; nach
|
damit User mehrere Items hintereinander importieren kann; nach
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||||
import { openLibrary } from '../lib/rhinoBridge'
|
import { openLibrary, pickTextureFile, onMessage } from '../lib/rhinoBridge'
|
||||||
|
|
||||||
/* Pill-Stil Field — Label klein in Caps, Inhalt darunter, optional Hint */
|
/* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */
|
||||||
function Field({ label, hint, children, style }) {
|
function Field({ label, hint, children, style }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4,
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4,
|
||||||
@@ -21,6 +21,34 @@ function Field({ label, hint, children, style }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* InlineNumberField — Label links, schmales Number-Input rechts (kompakt) */
|
||||||
|
function InlineNumberField({ label, hint, value, onChange, step, min, max, suffix }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '5px 0' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<span style={{ flex: 1, fontSize: 11, color: 'var(--text-primary)',
|
||||||
|
letterSpacing: '0.01em' }}>{label}</span>
|
||||||
|
<input type="number" value={value ?? ''}
|
||||||
|
step={step} min={min} max={max}
|
||||||
|
onChange={(ev) => onChange(parseFloat(ev.target.value))}
|
||||||
|
style={{ width: 80, height: BAR_H, padding: '0 10px',
|
||||||
|
fontSize: 11, textAlign: 'right' }} />
|
||||||
|
{suffix && (
|
||||||
|
<span style={{ width: 24, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hint && (
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
lineHeight: 1.4, marginTop: 3 }}>
|
||||||
|
{hint}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* Pill-Tabs — gleicher Stil wie BarToggle aus der Oberleiste */
|
/* Pill-Tabs — gleicher Stil wie BarToggle aus der Oberleiste */
|
||||||
function TabBar({ tabs, active, onChange }) {
|
function TabBar({ tabs, active, onChange }) {
|
||||||
return (
|
return (
|
||||||
@@ -39,36 +67,215 @@ function TabBar({ tabs, active, onChange }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
/* MaterialListRow — schmale Listen-Zeile links (ArchiCAD-Stil):
|
||||||
const isBuiltin = builtin || mat.source === 'builtin'
|
Color-Swatch + Name + Source-Badge. Click selektiert. */
|
||||||
|
function MaterialListRow({ mat, isBuiltin, isSelected, onSelect }) {
|
||||||
const isLibrary = mat.source === 'library'
|
const isLibrary = mat.source === 'library'
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div onClick={onSelect}
|
||||||
display: 'grid', gridTemplateColumns: '18px 1fr 92px 56px 22px',
|
style={{
|
||||||
|
display: 'grid', gridTemplateColumns: '14px 1fr 14px',
|
||||||
alignItems: 'center', gap: 6,
|
alignItems: 'center', gap: 6,
|
||||||
padding: '4px 8px',
|
padding: '4px 10px',
|
||||||
borderRadius: 6,
|
cursor: 'pointer',
|
||||||
background: isBuiltin ? 'var(--bg-section)' : 'transparent',
|
background: isSelected ? 'var(--accent-dim)' : 'transparent',
|
||||||
|
borderLeft: '2px solid ' + (isSelected ? 'var(--accent)' : 'transparent'),
|
||||||
transition: 'background 0.12s',
|
transition: 'background 0.12s',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'var(--bg-item-hover)' }}
|
onMouseEnter={(e) => {
|
||||||
onMouseLeave={(e) => { if (!isBuiltin) e.currentTarget.style.background = 'transparent' }}
|
if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
>
|
}}
|
||||||
<input type="color" value={mat.color || '#888888'}
|
onMouseLeave={(e) => {
|
||||||
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
if (!isSelected) e.currentTarget.style.background = 'transparent'
|
||||||
title="Farbe"
|
}}>
|
||||||
style={{ width: 18, height: 18, padding: 0, border: 'none',
|
<div style={{
|
||||||
background: 'transparent', cursor: 'pointer' }} />
|
width: 14, height: 14, borderRadius: 3,
|
||||||
|
background: mat.color || '#888888',
|
||||||
|
border: '1px solid var(--border-light)',
|
||||||
|
}} title={mat.color} />
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: isSelected ? 'var(--text-primary)' : 'var(--text-primary)',
|
||||||
|
fontWeight: isSelected ? 500 : 400,
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
|
}}>{mat.name || 'Unbenannt'}</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 9, fontWeight: 600,
|
||||||
|
color: isLibrary ? 'var(--accent)'
|
||||||
|
: isBuiltin ? 'var(--text-muted)'
|
||||||
|
: 'transparent',
|
||||||
|
textAlign: 'center',
|
||||||
|
}} title={isLibrary ? 'Library' : isBuiltin ? 'Builtin' : 'Lokal'}>
|
||||||
|
{isLibrary ? 'L' : isBuiltin ? 'B' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 (
|
||||||
|
<div style={{ padding: '4px 0' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<span style={{ flex: 1, fontSize: 11,
|
||||||
|
color: disabled ? 'var(--text-muted)' : 'var(--text-primary)' }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)', minWidth: 40,
|
||||||
|
textAlign: 'right' }}>
|
||||||
|
{display !== undefined ? display : v.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="range"
|
||||||
|
min={min} max={max} step={step} value={v}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(ev) => onChange(parseFloat(ev.target.value))}
|
||||||
|
style={{
|
||||||
|
width: '100%', marginTop: 4,
|
||||||
|
accentColor: 'var(--accent)',
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 (
|
||||||
|
<div style={{ padding: '5px 0' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 36, height: 36, borderRadius: 4,
|
||||||
|
background: hasPath
|
||||||
|
? `url(file://${tex.path}) center/cover, var(--bg-input)`
|
||||||
|
: 'var(--bg-input)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{!hasPath && (
|
||||||
|
<Icon name="image" size={14}
|
||||||
|
style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-primary)',
|
||||||
|
letterSpacing: '0.01em' }}>{label}</div>
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
marginTop: 2,
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap' }}
|
||||||
|
title={hasPath ? tex.path : ''}>
|
||||||
|
{hasPath ? filename : 'Keine Textur'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BarButton icon="folder_open"
|
||||||
|
onClick={() => pickTextureFile(slot)}
|
||||||
|
title="Datei waehlen"
|
||||||
|
disabled={disabled} />
|
||||||
|
{hasPath && (
|
||||||
|
<BarButton icon="close"
|
||||||
|
onClick={() => onChange(null)}
|
||||||
|
title="Textur entfernen"
|
||||||
|
disabled={disabled} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DetailSection — Section-Header + Body, immer offen (collapsible spaeter) */
|
||||||
|
function DetailSection({ title, children }) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 9, fontWeight: 600,
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||||
|
padding: '4px 0', marginBottom: 6,
|
||||||
|
borderBottom: '1px solid var(--border-light)',
|
||||||
|
}}>{title}</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MaterialDetail — rechte Seite (ArchiCAD-Stil): editiert das aktuell
|
||||||
|
ausgewaehlte Material. Builtin: Name read-only. */
|
||||||
|
function MaterialDetail({ mat, isBuiltin, hatchPatterns, onChange, onDelete }) {
|
||||||
|
if (!mat) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 40, textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||||
|
Kein Material ausgewaehlt.
|
||||||
|
<div style={{ marginTop: 8, fontSize: 10 }}>
|
||||||
|
Wähle links oder lege ein neues an.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const isLibrary = mat.source === 'library'
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '12px 14px',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
height: '100%', overflowY: 'auto' }}>
|
||||||
|
{/* Identitaet */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10,
|
||||||
|
marginBottom: 14 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 56, height: 56, borderRadius: 8,
|
||||||
|
background: mat.color || '#888888',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<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={isBuiltin}
|
disabled={isBuiltin}
|
||||||
placeholder="Name"
|
placeholder="Material-Name"
|
||||||
style={{ minWidth: 0, height: BAR_H, padding: '0 10px',
|
style={{ width: '100%', height: BAR_H, padding: '0 12px',
|
||||||
fontSize: 11,
|
fontSize: 12, fontWeight: 500,
|
||||||
opacity: isBuiltin ? 0.7 : 1 }} />
|
opacity: isBuiltin ? 0.7 : 1 }} />
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
marginTop: 4, letterSpacing: '0.04em' }}>
|
||||||
|
{isLibrary ? 'Aus Dossier-Library'
|
||||||
|
: isBuiltin ? 'Eingebaut (Builtin)'
|
||||||
|
: 'Lokales Material'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isBuiltin && onDelete && (
|
||||||
|
<BarButton icon="delete" onClick={onDelete}
|
||||||
|
title="Material loeschen" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailSection title="Oberflächenfarbe">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<input type="color" value={mat.color || '#888888'}
|
||||||
|
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
||||||
|
title="Farbe"
|
||||||
|
style={{ width: 32, height: BAR_H, padding: 0, border: 'none',
|
||||||
|
background: 'transparent', cursor: 'pointer' }} />
|
||||||
|
<input type="text"
|
||||||
|
value={mat.color || '#888888'}
|
||||||
|
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
||||||
|
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||||
|
fontSize: 11, fontFamily: 'var(--font-mono)' }} />
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
|
||||||
|
<DetailSection title="Schraffur (2D-Schnitt)">
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
<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={{ minWidth: 0, height: BAR_H, fontSize: 10 }}>
|
style={{ flex: 1, height: BAR_H, fontSize: 11 }}>
|
||||||
{(hatchPatterns || []).map(h => (
|
{(hatchPatterns || []).map(h => (
|
||||||
<option key={h} value={h}>{h}</option>
|
<option key={h} value={h}>{h}</option>
|
||||||
))}
|
))}
|
||||||
@@ -76,33 +283,65 @@ function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
|||||||
<input type="number" step="0.1" min="0.1"
|
<input type="number" step="0.1" min="0.1"
|
||||||
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="Skalierung"
|
||||||
style={{ minWidth: 0, height: BAR_H, padding: '0 10px',
|
style={{ width: 70, height: BAR_H, padding: '0 10px',
|
||||||
fontSize: 11, textAlign: 'right' }} />
|
fontSize: 11, textAlign: 'right' }} />
|
||||||
{isLibrary ? (
|
</div>
|
||||||
<span style={{ fontSize: 9, fontWeight: 600,
|
<div style={{ fontSize: 9, color: 'var(--text-muted)', marginTop: 4 }}>
|
||||||
color: 'var(--accent)',
|
Hatch-Pattern + Skalierung fuer die Sektion-Ansicht (Clipping Plane).
|
||||||
display: 'inline-flex', alignItems: 'center',
|
</div>
|
||||||
justifyContent: 'center' }}
|
</DetailSection>
|
||||||
title="Aus Dossier-Library">L</span>
|
|
||||||
) : isBuiltin ? (
|
<DetailSection title="Texturen (3D-Render)">
|
||||||
<span style={{ fontSize: 9, fontWeight: 600,
|
<TextureSlot label="Diffuse" slot="diffuse"
|
||||||
color: 'var(--text-muted)',
|
tex={mat.textures?.diffuse}
|
||||||
display: 'inline-flex', alignItems: 'center',
|
onChange={(t) => onChange({ ...mat, textures: {
|
||||||
justifyContent: 'center' }}
|
...(mat.textures || {}), diffuse: t } })}
|
||||||
title="Eingebaut">B</span>
|
disabled={isBuiltin} />
|
||||||
) : (
|
<TextureSlot label="Bump / Normal" slot="bump"
|
||||||
<button onClick={onDelete} title="Loeschen"
|
tex={mat.textures?.bump}
|
||||||
style={{ background: 'transparent', border: 'none',
|
onChange={(t) => onChange({ ...mat, textures: {
|
||||||
padding: 0, cursor: 'pointer',
|
...(mat.textures || {}), bump: t } })}
|
||||||
color: 'var(--text-muted)',
|
disabled={isBuiltin} />
|
||||||
display: 'inline-flex', alignItems: 'center',
|
<TextureSlot label="Roughness" slot="roughness"
|
||||||
justifyContent: 'center' }}
|
tex={mat.textures?.roughness}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--accent)'}
|
onChange={(t) => onChange({ ...mat, textures: {
|
||||||
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}>
|
...(mat.textures || {}), roughness: t } })}
|
||||||
<Icon name="close" size={14} />
|
disabled={isBuiltin} />
|
||||||
</button>
|
<TextureSlot label="Transparenz (Alpha)" slot="transparency"
|
||||||
)}
|
tex={mat.textures?.transparency}
|
||||||
|
onChange={(t) => onChange({ ...mat, textures: {
|
||||||
|
...(mat.textures || {}), transparency: t } })}
|
||||||
|
disabled={isBuiltin} />
|
||||||
|
<div style={{ marginTop: 4 }}>
|
||||||
|
<InlineNumberField label="UV-Skalierung"
|
||||||
|
value={mat.uvScaleM ?? 1.0}
|
||||||
|
step={0.1} min={0.01} suffix="m"
|
||||||
|
onChange={(v) => onChange({ ...mat, uvScaleM: v || 1.0 })}
|
||||||
|
hint="1 Welt-Meter ≙ wieviel Textur-Tile" />
|
||||||
|
</div>
|
||||||
|
</DetailSection>
|
||||||
|
|
||||||
|
<DetailSection title="Oberflächen-Eigenschaften (PBR)">
|
||||||
|
<Slider label="Rauheit (Roughness)"
|
||||||
|
value={mat.roughness ?? 0.7}
|
||||||
|
onChange={(v) => onChange({ ...mat, roughness: v })}
|
||||||
|
disabled={isBuiltin} />
|
||||||
|
<Slider label="Reflexion"
|
||||||
|
value={mat.reflection ?? 0.1}
|
||||||
|
onChange={(v) => onChange({ ...mat, reflection: v })}
|
||||||
|
disabled={isBuiltin} />
|
||||||
|
<Slider label="Transparenz"
|
||||||
|
value={mat.transparency ?? 0.0}
|
||||||
|
onChange={(v) => onChange({ ...mat, transparency: v })}
|
||||||
|
disabled={isBuiltin} />
|
||||||
|
<Slider label="IoR (Brechungsindex)"
|
||||||
|
value={mat.iorN ?? 1.0}
|
||||||
|
min={1.0} max={2.5} step={0.01}
|
||||||
|
display={(mat.iorN ?? 1.0).toFixed(2)}
|
||||||
|
onChange={(v) => onChange({ ...mat, iorN: v })}
|
||||||
|
disabled={isBuiltin} />
|
||||||
|
</DetailSection>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -115,9 +354,68 @@ export default function ProjectSettingsDialog({
|
|||||||
defaults: { ...(initial.defaults || {}) },
|
defaults: { ...(initial.defaults || {}) },
|
||||||
materials: [...(initial.materials || [])],
|
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 builtin = initial.builtinMaterials || []
|
||||||
const hatchPatterns = initial.hatchPatterns || ['Solid']
|
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) =>
|
const setDefault = (k, v) =>
|
||||||
setDraft(d => ({ ...d, defaults: { ...d.defaults, [k]: v } }))
|
setDraft(d => ({ ...d, defaults: { ...d.defaults, [k]: v } }))
|
||||||
|
|
||||||
@@ -127,7 +425,8 @@ export default function ProjectSettingsDialog({
|
|||||||
const delMat = (i) => setDraft(d => ({
|
const delMat = (i) => setDraft(d => ({
|
||||||
...d, materials: d.materials.filter((_, idx) => idx !== i),
|
...d, materials: d.materials.filter((_, idx) => idx !== i),
|
||||||
}))
|
}))
|
||||||
const addMat = () => setDraft(d => ({
|
const addMat = () => {
|
||||||
|
setDraft(d => ({
|
||||||
...d,
|
...d,
|
||||||
materials: [...d.materials, {
|
materials: [...d.materials, {
|
||||||
name: 'Neues Material', color: '#aaaaaa',
|
name: 'Neues Material', color: '#aaaaaa',
|
||||||
@@ -135,10 +434,8 @@ export default function ProjectSettingsDialog({
|
|||||||
source: 'local', libraryId: null,
|
source: 'local', libraryId: null,
|
||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
|
// Direkt selektieren — User kann gleich editieren
|
||||||
const numberInputStyle = {
|
setSelMat({ kind: 'local', idx: draft.materials.length })
|
||||||
flex: 1, height: BAR_H, padding: '0 12px',
|
|
||||||
fontSize: 11, textAlign: 'right',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapperStyle = embedded ? {
|
const wrapperStyle = embedded ? {
|
||||||
@@ -166,82 +463,120 @@ export default function ProjectSettingsDialog({
|
|||||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
||||||
padding: '8px 14px' }}>
|
padding: '8px 14px' }}>
|
||||||
{tab === 'defaults' && (
|
{tab === 'defaults' && (
|
||||||
<>
|
<div style={{ maxWidth: 520 }}>
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||||
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
||||||
Voreinstellungen fuer neue Elemente. Pro-Element editierte
|
Voreinstellungen fuer neue Elemente. Pro-Element editierte
|
||||||
Werte bleiben davon unberuehrt.
|
Werte bleiben davon unberuehrt.
|
||||||
</div>
|
</div>
|
||||||
<Field label="Standard-Geschosshöhe (m)"
|
<DetailSection title="Geschoss">
|
||||||
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
|
<InlineNumberField label="Standard-Geschosshöhe"
|
||||||
<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)}
|
step={0.05} min={1.0} max={10} suffix="m"
|
||||||
style={numberInputStyle} />
|
onChange={(v) => setDefault('geschossHoehe', v || 3.0)}
|
||||||
</Field>
|
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar" />
|
||||||
<Field label="Standard-Schnitthöhe (m)"
|
<InlineNumberField label="Standard-Schnitthöhe"
|
||||||
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}
|
value={draft.defaults.schnitthoehe ?? 1.0}
|
||||||
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
step={0.05} min={0.1} max={3} suffix="m"
|
||||||
style={numberInputStyle} />
|
onChange={(v) => setDefault('schnitthoehe', v || 1.0)}
|
||||||
</Field>
|
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses" />
|
||||||
<div style={{ height: 1, background: 'var(--border-light)',
|
</DetailSection>
|
||||||
margin: '12px 0' }} />
|
<DetailSection title="Schnitte / Ansichten">
|
||||||
<Field label="Standard-Schnitt-Tiefe hinten (m)"
|
<InlineNumberField label="Standard-Tiefe hinten"
|
||||||
hint="Default-Tiefe fuer neue Schnitte/Ansichten">
|
|
||||||
<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)}
|
step={0.5} min={0.5} suffix="m"
|
||||||
style={numberInputStyle} />
|
onChange={(v) => setDefault('schnittDepthBack', v || 8.0)}
|
||||||
</Field>
|
hint="Default-Tiefe fuer neue Schnitte/Ansichten" />
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<InlineNumberField label="Höhe unten"
|
||||||
<Field label="Höhe unten (m)" style={{ flex: 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))}
|
step={0.1} suffix="m"
|
||||||
style={numberInputStyle} />
|
onChange={(v) => setDefault('schnittHeightMin', v)} />
|
||||||
</Field>
|
<InlineNumberField label="Höhe oben"
|
||||||
<Field label="Höhe oben (m)" style={{ flex: 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))}
|
step={0.1} suffix="m"
|
||||||
style={numberInputStyle} />
|
onChange={(v) => setDefault('schnittHeightMax', v)} />
|
||||||
</Field>
|
</DetailSection>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'materials' && (
|
{tab === 'materials' && (
|
||||||
<>
|
<div style={{ display: 'flex', height: '100%',
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
margin: '-8px -14px', /* Tab-Padding aufheben */
|
||||||
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
minHeight: 0 }}>
|
||||||
Eingebaute Materialien (B) — Farbe + Hatch anpassbar, Name fix.
|
{/* Links: Liste */}
|
||||||
Library-Materialien (L) aus Dossier-Library importiert.
|
<div style={{
|
||||||
Lokale Materialien frei editierbar.
|
width: 240, flexShrink: 0,
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
borderRight: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-dialog)',
|
||||||
|
}}>
|
||||||
|
<div style={{ padding: '8px 10px',
|
||||||
|
borderBottom: '1px solid var(--border-light)' }}>
|
||||||
|
<input type="text" value={matSearch}
|
||||||
|
onChange={(ev) => setMatSearch(ev.target.value)}
|
||||||
|
placeholder="Suchen…"
|
||||||
|
style={{ width: '100%', height: BAR_H,
|
||||||
|
padding: '0 12px', fontSize: 11,
|
||||||
|
boxSizing: 'border-box' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, overflowY: 'auto',
|
||||||
|
padding: '4px 0' }}>
|
||||||
|
{filteredBuiltin.length > 0 && (
|
||||||
|
<div style={{ fontSize: 8, fontWeight: 600,
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
padding: '6px 12px 2px',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase' }}>
|
||||||
|
Eingebaut
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filteredBuiltin.map((m) => (
|
||||||
|
<MaterialListRow key={'b_' + m.name}
|
||||||
|
mat={m} isBuiltin
|
||||||
|
isSelected={selMat?.kind === 'builtin' && selMat.name === m.name}
|
||||||
|
onSelect={() => setSelMat({ kind: 'builtin', name: m.name })} />
|
||||||
|
))}
|
||||||
|
{filteredLocal.length > 0 && (
|
||||||
|
<div style={{ fontSize: 8, fontWeight: 600,
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
padding: '10px 12px 2px',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase' }}>
|
||||||
|
Projekt
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filteredLocal.map(({ m, i }) => (
|
||||||
|
<MaterialListRow key={'u_' + i}
|
||||||
|
mat={m}
|
||||||
|
isSelected={selMat?.kind === 'local' && selMat.idx === i}
|
||||||
|
onSelect={() => setSelMat({ kind: 'local', idx: i })} />
|
||||||
|
))}
|
||||||
|
{filteredBuiltin.length === 0 && filteredLocal.length === 0 && (
|
||||||
|
<div style={{ padding: 20, textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)', fontSize: 10 }}>
|
||||||
|
Nichts gefunden.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 4,
|
||||||
|
padding: '6px 8px',
|
||||||
|
borderTop: '1px solid var(--border-light)' }}>
|
||||||
|
<BarToggle icon="add" onClick={addMat}
|
||||||
|
title="Neues Material" />
|
||||||
|
<BarToggle icon="inventory_2" onClick={openLibrary}
|
||||||
|
title="Aus Library importieren" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Rechts: Detail */}
|
||||||
|
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||||
|
<MaterialDetail
|
||||||
|
mat={selectedMat}
|
||||||
|
isBuiltin={selectedIsBuiltin}
|
||||||
|
hatchPatterns={hatchPatterns}
|
||||||
|
onChange={updateSelected}
|
||||||
|
onDelete={selMat?.kind === 'local' ? deleteSelected : null} />
|
||||||
</div>
|
</div>
|
||||||
{builtin.map((m) => (
|
|
||||||
<MaterialRow key={'b_' + m.name}
|
|
||||||
mat={m}
|
|
||||||
builtin
|
|
||||||
hatchPatterns={hatchPatterns}
|
|
||||||
onChange={() => {/* read-only Phase 1 */}} />
|
|
||||||
))}
|
|
||||||
{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: 12 }}>
|
|
||||||
<BarToggle icon="add" label="Material" onClick={addMat}
|
|
||||||
title="Neues Material anlegen" />
|
|
||||||
<BarToggle icon="inventory_2" label="Library"
|
|
||||||
onClick={openLibrary}
|
|
||||||
title="Material aus Dossier-Library importieren" />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ export function toggleGridVisible(on) { send('TOGGLE_GRID_VISIBLE', { visible: !
|
|||||||
export function openProjectSettings() { send('OPEN_PROJECT_SETTINGS', {}) }
|
export function openProjectSettings() { send('OPEN_PROJECT_SETTINGS', {}) }
|
||||||
// Dossier-Library (Material-/Symbol-/Object-Templates, Phase A: lokal+material)
|
// Dossier-Library (Material-/Symbol-/Object-Templates, Phase A: lokal+material)
|
||||||
export function openLibrary() { send('OPEN_LIBRARY', {}) }
|
export function openLibrary() { send('OPEN_LIBRARY', {}) }
|
||||||
|
// Material-Textur: macOS-File-Picker oeffnen, Antwort kommt via
|
||||||
|
// TEXTURE_PICKED-Message mit {slot, path}.
|
||||||
|
export function pickTextureFile(slot) {
|
||||||
|
send('PICK_TEXTURE_FILE', { slot: slot || 'diffuse' })
|
||||||
|
}
|
||||||
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
||||||
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
||||||
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
||||||
|
|||||||
Reference in New Issue
Block a user