Projekt-Settings-Dialog + Library Phase A + Material-Merger
- Project-Settings-Dialog (Voreinstellungen Geschoss/Schnitt + Material-Editor) ueber Zahnrad-Icon in Oberleiste; Defaults werden in schnitte.pick_schnitt + GeschossManager als Vorgabe genommen, pro-Element-Werte unangetastet - Dossier-Library Phase A (lokal, read-only): rhino/library.py + LibraryBrowser Satellite; Seed-Manifest unter ~/Library/Application Support/Dossier/library/ - Material-Merger: _get_all_materials(doc) merged builtin _MATERIAL_LIBRARY mit Projekt-Settings-Materialien (inkl. Library-Imports); Wand-Erstellung, Sub-Layer-Anlage + Elemente-Material-Dropdown ziehen jetzt aus dem Merge Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+30
-5
@@ -2038,6 +2038,29 @@ _MATERIAL_LIBRARY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_all_materials(doc):
|
||||||
|
"""Builtin _MATERIAL_LIBRARY + Projekt-Settings-Materialien gemerged.
|
||||||
|
Returns dict[name] -> {color, hatch, scale}. Projekt-Settings (inkl.
|
||||||
|
Library-Imports) ueberschreibt builtin bei Namensgleichheit, sodass
|
||||||
|
der User builtin-Defaults pro Projekt anpassen kann."""
|
||||||
|
merged = {n: dict(m) for n, m in _MATERIAL_LIBRARY.items()}
|
||||||
|
try:
|
||||||
|
import rhinopanel
|
||||||
|
ps = rhinopanel.load_project_settings(doc) if doc else None
|
||||||
|
if isinstance(ps, dict):
|
||||||
|
for m in ps.get("materials", []):
|
||||||
|
n = m.get("name")
|
||||||
|
if not n: continue
|
||||||
|
merged[n] = {
|
||||||
|
"color": m.get("color", "#888888"),
|
||||||
|
"hatch": m.get("hatch", "Solid"),
|
||||||
|
"scale": float(m.get("scale", 1.0) or 1.0),
|
||||||
|
}
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] _get_all_materials:", ex)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
def _set_layer_section_hatch(doc, layer_idx, hatch_name, scale=1.0,
|
def _set_layer_section_hatch(doc, layer_idx, hatch_name, scale=1.0,
|
||||||
rotation=0.0):
|
rotation=0.0):
|
||||||
"""Konfiguriert Rhinos native Section-Hatch-Properties am Layer.
|
"""Konfiguriert Rhinos native Section-Hatch-Properties am Layer.
|
||||||
@@ -2073,9 +2096,10 @@ def _ensure_material_sublayer(doc, geschoss_name, material_name):
|
|||||||
mit Material-Farbe + Section-Hatch konfiguriert. Liefert Layer-Index.
|
mit Material-Farbe + Section-Hatch konfiguriert. Liefert Layer-Index.
|
||||||
Bei leerem oder unbekanntem Material: Fallback auf das normale
|
Bei leerem oder unbekanntem Material: Fallback auf das normale
|
||||||
Wand-Volume-Layer (= Standard fuer Solid-Waende)."""
|
Wand-Volume-Layer (= Standard fuer Solid-Waende)."""
|
||||||
if not material_name or material_name not in _MATERIAL_LIBRARY:
|
all_mats = _get_all_materials(doc)
|
||||||
|
if not material_name or material_name not in all_mats:
|
||||||
return _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
return _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
||||||
mat = _MATERIAL_LIBRARY[material_name]
|
mat = all_mats[material_name]
|
||||||
parent_path = _layer_path_volume(doc, geschoss_name)
|
parent_path = _layer_path_volume(doc, geschoss_name)
|
||||||
full_path = "{}::{}".format(parent_path, material_name)
|
full_path = "{}::{}".format(parent_path, material_name)
|
||||||
idx = _ensure_layer(doc, full_path)
|
idx = _ensure_layer(doc, full_path)
|
||||||
@@ -5127,8 +5151,9 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
|||||||
mat_name = lay_def.get("material", "") if is_layered else ""
|
mat_name = lay_def.get("material", "") if is_layered else ""
|
||||||
effective_color = color
|
effective_color = color
|
||||||
target_layer = layer
|
target_layer = layer
|
||||||
if mat_name and mat_name in _MATERIAL_LIBRARY:
|
all_mats = _get_all_materials(doc)
|
||||||
effective_color = _MATERIAL_LIBRARY[mat_name]["color"]
|
if mat_name and mat_name in all_mats:
|
||||||
|
effective_color = all_mats[mat_name]["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
|
||||||
@@ -5838,7 +5863,7 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
"materials": [
|
"materials": [
|
||||||
{"name": n, "color": m["color"],
|
{"name": n, "color": m["color"],
|
||||||
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
|
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
|
||||||
for n, m in _MATERIAL_LIBRARY.items()],
|
for n, m in _get_all_materials(doc).items()],
|
||||||
"oeffStyles": list_oeff_styles(doc),
|
"oeffStyles": list_oeff_styles(doc),
|
||||||
}
|
}
|
||||||
self.send("STATE", payload)
|
self.send("STATE", payload)
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
library.py — Dossier-Library (Phase A: lokal, read-only)
|
||||||
|
|
||||||
|
Library = wiederverwendbare Material-/Symbol-/Object-Templates die ein User
|
||||||
|
oder ein Team teilt. Phase A: lokaler Ordner mit library.json + Assets.
|
||||||
|
Spaeter (Phase B/C): Cloud-Sync via GitHub-Releases.
|
||||||
|
|
||||||
|
Library-Root: ~/Library/Application Support/Dossier/library/
|
||||||
|
Struktur:
|
||||||
|
library/
|
||||||
|
library.json # Manifest (Schema-Version + items[])
|
||||||
|
previews/ # PNG-Thumbnails
|
||||||
|
assets/ # .3dm-Fragmente fuer symbol/object
|
||||||
|
|
||||||
|
Manifest-Item:
|
||||||
|
{
|
||||||
|
"id": "mat-beton-roh-v1", # global eindeutig (UUID/Slug)
|
||||||
|
"type": "material", # material | symbol | object
|
||||||
|
"name": "Beton roh",
|
||||||
|
"version": 1, # Schema-Version des Items
|
||||||
|
"tags": ["beton", "tragwerk"],
|
||||||
|
"preview": "previews/mat-beton-roh.png",
|
||||||
|
"data": { ... } # Typ-spezifische Felder
|
||||||
|
}
|
||||||
|
|
||||||
|
Material-data: { color, hatch, scale }
|
||||||
|
Symbol/Object-data: { files: ["assets/sym-foo.3dm"] }
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Lazy Import von Rhino — library.py soll auch in einem Test-Skript ohne
|
||||||
|
# Rhino-Kontext importierbar sein (z.B. fuer Seed-Logik).
|
||||||
|
try:
|
||||||
|
import Rhino # noqa: F401
|
||||||
|
except Exception:
|
||||||
|
Rhino = None
|
||||||
|
|
||||||
|
|
||||||
|
_LIB_DIRNAME = "library"
|
||||||
|
_MANIFEST_FN = "library.json"
|
||||||
|
_SCHEMA_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
def library_root():
|
||||||
|
"""Mac-Pfad zur lokalen Library. Wird bei Bedarf angelegt."""
|
||||||
|
base = os.path.expanduser(
|
||||||
|
"~/Library/Application Support/Dossier")
|
||||||
|
return os.path.join(base, _LIB_DIRNAME)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_library():
|
||||||
|
"""Legt Library-Folder + Sub-Folders + Seed-Manifest an wenn leer."""
|
||||||
|
root = library_root()
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
try: os.makedirs(root)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] mkdir:", ex); return root
|
||||||
|
for sub in ("previews", "assets"):
|
||||||
|
p = os.path.join(root, sub)
|
||||||
|
if not os.path.isdir(p):
|
||||||
|
try: os.makedirs(p)
|
||||||
|
except Exception: pass
|
||||||
|
manifest_path = os.path.join(root, _MANIFEST_FN)
|
||||||
|
if not os.path.isfile(manifest_path):
|
||||||
|
_write_seed_manifest(manifest_path)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def _write_seed_manifest(path):
|
||||||
|
"""Bootstrappt 4 typische Architektur-Materialien als Start."""
|
||||||
|
seed = {
|
||||||
|
"schemaVersion": _SCHEMA_VERSION,
|
||||||
|
"name": "Dossier-Library (lokal)",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "mat-beton-sichtbeton-v1",
|
||||||
|
"type": "material", "version": 1,
|
||||||
|
"name": "Beton — Sichtbeton",
|
||||||
|
"tags": ["beton", "tragwerk", "roh"],
|
||||||
|
"data": {"color": "#a8a39b", "hatch": "Solid", "scale": 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mat-mauerwerk-backstein-v1",
|
||||||
|
"type": "material", "version": 1,
|
||||||
|
"name": "Mauerwerk — Backstein",
|
||||||
|
"tags": ["mauerwerk", "stein"],
|
||||||
|
"data": {"color": "#a45a3c", "hatch": "Solid", "scale": 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mat-daemmung-mineralwolle-v1",
|
||||||
|
"type": "material", "version": 1,
|
||||||
|
"name": "Daemmung — Mineralwolle",
|
||||||
|
"tags": ["daemmung", "weich"],
|
||||||
|
"data": {"color": "#e8d36b", "hatch": "Solid", "scale": 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mat-holz-fichte-v1",
|
||||||
|
"type": "material", "version": 1,
|
||||||
|
"name": "Holz — Fichte",
|
||||||
|
"tags": ["holz", "ausbau"],
|
||||||
|
"data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(seed, f, indent=2, ensure_ascii=False)
|
||||||
|
print("[LIBRARY] Seed geschrieben:", path)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] seed write:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def load_manifest():
|
||||||
|
"""Liest library.json — legt Library bei Bedarf an. Returns dict mit
|
||||||
|
schemaVersion + name + items[]. Bei Fehler leeres Manifest."""
|
||||||
|
ensure_library()
|
||||||
|
path = os.path.join(library_root(), _MANIFEST_FN)
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict): return _empty_manifest()
|
||||||
|
items = data.get("items")
|
||||||
|
if not isinstance(items, list): data["items"] = []
|
||||||
|
else: data["items"] = [_normalize_item(x) for x in items
|
||||||
|
if _normalize_item(x) is not None]
|
||||||
|
return data
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] load_manifest:", ex)
|
||||||
|
return _empty_manifest()
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_manifest():
|
||||||
|
return {"schemaVersion": _SCHEMA_VERSION,
|
||||||
|
"name": "Dossier-Library", "items": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_item(it):
|
||||||
|
if not isinstance(it, dict): return None
|
||||||
|
if not it.get("id") or not it.get("type"): return None
|
||||||
|
out = {
|
||||||
|
"id": str(it["id"]),
|
||||||
|
"type": str(it["type"]),
|
||||||
|
"name": str(it.get("name") or "Unbenannt"),
|
||||||
|
"version": int(it.get("version") or 1),
|
||||||
|
"tags": list(it.get("tags") or []),
|
||||||
|
"preview": it.get("preview"),
|
||||||
|
"data": it.get("data") or {},
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def find_item(item_id):
|
||||||
|
"""Sucht ein Item per id im Manifest. Returns None wenn nicht da."""
|
||||||
|
if not item_id: return None
|
||||||
|
for it in load_manifest().get("items", []):
|
||||||
|
if it.get("id") == item_id: return it
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --- Import-Logik -----------------------------------------------------------
|
||||||
|
|
||||||
|
def import_material(doc, item):
|
||||||
|
"""Importiert ein Material-Item in die Projekt-Settings des Doc. Dedupe
|
||||||
|
per libraryId — wenn schon importiert, kein Doppel-Eintrag.
|
||||||
|
Returns (ok, message)."""
|
||||||
|
if doc is None: return False, "Kein aktives Dokument"
|
||||||
|
if not isinstance(item, dict) or item.get("type") != "material":
|
||||||
|
return False, "Item ist kein Material"
|
||||||
|
data = item.get("data") or {}
|
||||||
|
new_mat = {
|
||||||
|
"name": item.get("name") or "Unbenannt",
|
||||||
|
"color": data.get("color", "#888888"),
|
||||||
|
"hatch": data.get("hatch", "Solid"),
|
||||||
|
"scale": float(data.get("scale", 1.0) or 1.0),
|
||||||
|
"source": "library",
|
||||||
|
"libraryId": item.get("id"),
|
||||||
|
}
|
||||||
|
# Lazy-Import um Zyklen zu vermeiden
|
||||||
|
import rhinopanel
|
||||||
|
settings = rhinopanel.load_project_settings(doc)
|
||||||
|
mats = list(settings.get("materials", []))
|
||||||
|
for m in mats:
|
||||||
|
if m.get("libraryId") == new_mat["libraryId"]:
|
||||||
|
return False, "Material bereits importiert"
|
||||||
|
mats.append(new_mat)
|
||||||
|
settings["materials"] = mats
|
||||||
|
rhinopanel.save_project_settings(doc, settings)
|
||||||
|
return True, "Material importiert"
|
||||||
|
|
||||||
|
|
||||||
|
def import_item(doc, item_id):
|
||||||
|
"""Type-dispatching Import. Phase A: nur material. symbol/object kommen
|
||||||
|
spaeter via File3dm.Read + InstanceDefinition."""
|
||||||
|
item = find_item(item_id)
|
||||||
|
if item is None: return False, "Item nicht gefunden: " + str(item_id)
|
||||||
|
t = item.get("type")
|
||||||
|
if t == "material":
|
||||||
|
return import_material(doc, item)
|
||||||
|
# Phase A: symbol/object noch nicht
|
||||||
|
return False, "Typ '{}' wird erst in Phase A.2 unterstuetzt".format(t)
|
||||||
@@ -96,6 +96,7 @@ def _broadcast_state(doc=None, hatch_patterns=None):
|
|||||||
"zeichnungsebenen": json.loads(z_raw) if z_raw else None,
|
"zeichnungsebenen": json.loads(z_raw) if z_raw else None,
|
||||||
"ebenen": json.loads(e_raw) if e_raw else None,
|
"ebenen": json.loads(e_raw) if e_raw else None,
|
||||||
"projectZeroMum": zero_mum,
|
"projectZeroMum": zero_mum,
|
||||||
|
"projectSettings": load_project_settings(doc),
|
||||||
"hatchPatterns": hatch_patterns if hatch_patterns is not None
|
"hatchPatterns": hatch_patterns if hatch_patterns is not None
|
||||||
else _hatch_pattern_names(doc),
|
else _hatch_pattern_names(doc),
|
||||||
"layerCombinations": list_layer_preset_names(doc),
|
"layerCombinations": list_layer_preset_names(doc),
|
||||||
@@ -120,6 +121,83 @@ def _broadcast_state(doc=None, hatch_patterns=None):
|
|||||||
_PRESETS_KEY = "dossier_layer_presets"
|
_PRESETS_KEY = "dossier_layer_presets"
|
||||||
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
|
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
|
||||||
|
|
||||||
|
# Projekt-Einstellungen: zentrale Voreinstellungen die beim Erstellen
|
||||||
|
# neuer Geschosse / Schnitte / etc. als Default genommen werden. Pro-
|
||||||
|
# Element-Werte ueberschreiben das natuerlich — das hier ist nur die
|
||||||
|
# Voreinstellung. Persistiert pro Doc.
|
||||||
|
_PROJECT_SETTINGS_KEY = "dossier_project_settings"
|
||||||
|
|
||||||
|
_PROJECT_SETTINGS_DEFAULTS = {
|
||||||
|
"defaults": {
|
||||||
|
"geschossHoehe": 3.0,
|
||||||
|
"schnitthoehe": 1.0,
|
||||||
|
"schnittDepthBack": 8.0,
|
||||||
|
"schnittHeightMin": -1.0,
|
||||||
|
"schnittHeightMax": 12.0,
|
||||||
|
},
|
||||||
|
"materials": [], # User-erweiterte Materialien (zusaetzlich zur
|
||||||
|
# hardcoded _MATERIAL_LIBRARY in elemente.py)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_material(m):
|
||||||
|
"""Garantiert Material-Schema: name + color + hatch + scale + source
|
||||||
|
+ libraryId. source: 'local' | 'library' | 'builtin'. libraryId: UUID
|
||||||
|
wenn aus Library, sonst null. Felder fehlen bei alten Eintraegen — wir
|
||||||
|
setzen Defaults damit Frontend nicht null-checken muss."""
|
||||||
|
if not isinstance(m, dict): return None
|
||||||
|
return {
|
||||||
|
"name": m.get("name") or "Unbenannt",
|
||||||
|
"color": m.get("color") or "#888888",
|
||||||
|
"hatch": m.get("hatch") or "Solid",
|
||||||
|
"scale": float(m.get("scale", 1.0) or 1.0),
|
||||||
|
"source": m.get("source") or "local",
|
||||||
|
"libraryId": m.get("libraryId"), # None wenn nicht aus Library
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_project_settings(doc):
|
||||||
|
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn
|
||||||
|
Felder fehlen. Garantiert dass `defaults` und `materials` immer da
|
||||||
|
sind, und Materialien normalisiert (source + libraryId)."""
|
||||||
|
raw = None
|
||||||
|
try: raw = doc.Strings.GetValue(_PROJECT_SETTINGS_KEY) if doc else None
|
||||||
|
except Exception: raw = None
|
||||||
|
out = {
|
||||||
|
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
|
||||||
|
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]),
|
||||||
|
}
|
||||||
|
if not raw: return out
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
d = data.get("defaults")
|
||||||
|
if isinstance(d, dict):
|
||||||
|
for k, v in d.items():
|
||||||
|
out["defaults"][k] = v
|
||||||
|
m = data.get("materials")
|
||||||
|
if isinstance(m, list):
|
||||||
|
out["materials"] = [
|
||||||
|
_normalize_material(x) for x in m
|
||||||
|
if _normalize_material(x) is not None
|
||||||
|
]
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PROJECT-SETTINGS] load:", ex)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_project_settings(doc, settings):
|
||||||
|
"""Persistiert Settings in doc.Strings. settings: dict mit
|
||||||
|
'defaults' + 'materials'. Caller broadcastet ggf. selbst."""
|
||||||
|
if doc is None or not isinstance(settings, dict): return False
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_PROJECT_SETTINGS_KEY,
|
||||||
|
json.dumps(settings, ensure_ascii=False))
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PROJECT-SETTINGS] save:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def load_layer_presets(doc):
|
def load_layer_presets(doc):
|
||||||
raw = doc.Strings.GetValue(_PRESETS_KEY)
|
raw = doc.Strings.GetValue(_PRESETS_KEY)
|
||||||
@@ -531,9 +609,131 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
try: open_layer_combinations_window()
|
try: open_layer_combinations_window()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] open layer-combinations:", ex)
|
print("[EBENEN] open layer-combinations:", ex)
|
||||||
|
elif t == "OPEN_PROJECT_SETTINGS":
|
||||||
|
try: self._open_project_settings()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] open project-settings:", ex)
|
||||||
|
elif t == "OPEN_LIBRARY":
|
||||||
|
try: self._open_library()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[EBENEN] open library:", ex)
|
||||||
|
|
||||||
# ---- Helpers ----
|
# ---- Helpers ----
|
||||||
|
|
||||||
|
def _open_project_settings(self):
|
||||||
|
"""Oeffnet React-Satellite mit Projekt-Voreinstellungen (Geschoss-/
|
||||||
|
Schnitt-Defaults + Material-Library). Werte werden nur als
|
||||||
|
Voreinstellung beim Erstellen neuer Elemente genutzt — pro-Element
|
||||||
|
editierte Werte bleiben davon unberuehrt."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
current = load_project_settings(doc)
|
||||||
|
# Hardcoded Material-Library aus elemente.py mit-laden damit
|
||||||
|
# Frontend die Defaults zeigt + User sie ggf. anpassen kann.
|
||||||
|
try:
|
||||||
|
import elemente
|
||||||
|
built_in = []
|
||||||
|
for name, m in elemente._MATERIAL_LIBRARY.items():
|
||||||
|
built_in.append({
|
||||||
|
"name": name,
|
||||||
|
"color": m.get("color", "#888888"),
|
||||||
|
"hatch": m.get("hatch", "Solid"),
|
||||||
|
"scale": float(m.get("scale", 1.0)),
|
||||||
|
"source": "builtin",
|
||||||
|
"libraryId": None,
|
||||||
|
"builtin": True, # legacy flag fuer aelteren UI-Code
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
built_in = []
|
||||||
|
params = {
|
||||||
|
"defaults": current.get("defaults", {}),
|
||||||
|
"materials": current.get("materials", []),
|
||||||
|
"builtinMaterials": built_in,
|
||||||
|
"hatchPatterns": _hatch_pattern_names(doc),
|
||||||
|
}
|
||||||
|
def on_save(updated):
|
||||||
|
doc2 = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc2 is None: return
|
||||||
|
new_settings = {
|
||||||
|
"defaults": updated.get("defaults", {}),
|
||||||
|
"materials": updated.get("materials", []),
|
||||||
|
}
|
||||||
|
save_project_settings(doc2, new_settings)
|
||||||
|
_broadcast_state(doc2)
|
||||||
|
try:
|
||||||
|
import scriptcontext as sc
|
||||||
|
eb = sc.sticky.get("elemente_bridge")
|
||||||
|
if eb is not None: eb._send_state()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PROJECT-SETTINGS] elemente refresh:", ex)
|
||||||
|
panel_base.open_satellite_window(
|
||||||
|
"project_settings",
|
||||||
|
params=params,
|
||||||
|
title="Projekt-Einstellungen",
|
||||||
|
size=(440, 540),
|
||||||
|
on_save=on_save)
|
||||||
|
|
||||||
|
def _open_library(self):
|
||||||
|
"""Oeffnet den Library-Browser als Satellite. Bridge bleibt offen
|
||||||
|
damit User mehrere Items hintereinander importieren kann; nach
|
||||||
|
jedem Import wird der Material-Status zurueckgemeldet damit das
|
||||||
|
UI 'importiert' anzeigt."""
|
||||||
|
import library
|
||||||
|
bridge_holder = {"form": None}
|
||||||
|
class _LibraryBridge(panel_base.BaseBridge):
|
||||||
|
def __init__(self):
|
||||||
|
panel_base.BaseBridge.__init__(self, "library")
|
||||||
|
def _on_ready(self):
|
||||||
|
self._send_state()
|
||||||
|
def _send_state(self):
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
manifest = library.load_manifest()
|
||||||
|
imported_ids = set()
|
||||||
|
if doc is not None:
|
||||||
|
settings = load_project_settings(doc)
|
||||||
|
for m in settings.get("materials", []):
|
||||||
|
lid = m.get("libraryId")
|
||||||
|
if lid: imported_ids.add(lid)
|
||||||
|
self.send("LIBRARY_STATE", {
|
||||||
|
"manifest": manifest,
|
||||||
|
"importedIds": list(imported_ids),
|
||||||
|
"libraryRoot": library.library_root(),
|
||||||
|
})
|
||||||
|
def handle(self, data):
|
||||||
|
if not isinstance(data, dict): return
|
||||||
|
t = data.get("type", "")
|
||||||
|
p = data.get("payload") or {}
|
||||||
|
if t == "READY":
|
||||||
|
self._on_ready()
|
||||||
|
elif t == "IMPORT_ITEM":
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
ok, msg = library.import_item(doc, p.get("id"))
|
||||||
|
print("[LIBRARY] import {}: {} ({})".format(
|
||||||
|
p.get("id"), ok, msg))
|
||||||
|
if ok:
|
||||||
|
_broadcast_state(doc)
|
||||||
|
# Elemente-Panel-Dropdown muss neu laden (materials)
|
||||||
|
try:
|
||||||
|
import scriptcontext as sc
|
||||||
|
eb = sc.sticky.get("elemente_bridge")
|
||||||
|
if eb is not None: eb._send_state()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] elemente refresh:", ex)
|
||||||
|
self._send_state()
|
||||||
|
elif t == "RELOAD":
|
||||||
|
self._send_state()
|
||||||
|
elif t == "CLOSE":
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
b = _LibraryBridge()
|
||||||
|
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||||
|
"library",
|
||||||
|
params={},
|
||||||
|
title="Dossier-Library",
|
||||||
|
size=(640, 560),
|
||||||
|
bridge=b)
|
||||||
|
|
||||||
def _open_geschoss_settings(self, geschoss):
|
def _open_geschoss_settings(self, geschoss):
|
||||||
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form mit WebView) mit dem
|
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form mit WebView) mit dem
|
||||||
GeschossSettingsDialog. Save updated den Eintrag in doc.Strings +
|
GeschossSettingsDialog. Save updated den Eintrag in doc.Strings +
|
||||||
|
|||||||
+17
-4
@@ -589,12 +589,25 @@ def pick_schnitt_interactive(doc, defaults=None):
|
|||||||
"""Interaktiver Pick: 2 Punkte + Dir-Pfeil + Defaults aus settings.
|
"""Interaktiver Pick: 2 Punkte + Dir-Pfeil + Defaults aus settings.
|
||||||
Liefert die neue Schnitt-Id oder None bei Abbruch.
|
Liefert die neue Schnitt-Id oder None bei Abbruch.
|
||||||
|
|
||||||
defaults: {depthBack, heightMin, heightMax, cutAtLine, namePrefix}"""
|
defaults: {depthBack, heightMin, heightMax, cutAtLine, namePrefix}
|
||||||
|
Wenn ein Feld fehlt, wird aus dossier_project_settings nachgeschaut
|
||||||
|
(zentrale Project-Voreinstellung), sonst Hardcoded-Default."""
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
|
# Project-Defaults als 2-stufiges Fallback (defaults > project > hardcoded)
|
||||||
|
proj_d = {}
|
||||||
|
try:
|
||||||
|
import rhinopanel
|
||||||
|
ps = rhinopanel.load_project_settings(doc) or {}
|
||||||
|
proj_d = ps.get("defaults", {}) or {}
|
||||||
|
except Exception: pass
|
||||||
|
def _resolve(key, proj_key, fallback):
|
||||||
|
if key in defaults: return defaults[key]
|
||||||
|
if proj_key in proj_d: return proj_d[proj_key]
|
||||||
|
return fallback
|
||||||
name_prefix = defaults.get("namePrefix", "S")
|
name_prefix = defaults.get("namePrefix", "S")
|
||||||
depth_back = float(defaults.get("depthBack", 8.0))
|
depth_back = float(_resolve("depthBack", "schnittDepthBack", 8.0))
|
||||||
h_min = float(defaults.get("heightMin", -1.0))
|
h_min = float(_resolve("heightMin", "schnittHeightMin", -1.0))
|
||||||
h_max = float(defaults.get("heightMax", 12.0))
|
h_max = float(_resolve("heightMax", "schnittHeightMax", 12.0))
|
||||||
cut_at_line = bool(defaults.get("cutAtLine", True))
|
cut_at_line = bool(defaults.get("cutAtLine", True))
|
||||||
|
|
||||||
# Pick Punkt 1
|
# Pick Punkt 1
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import LibraryBrowser from './components/LibraryBrowser'
|
||||||
|
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
|
||||||
|
|
||||||
|
export default function LibraryApp() {
|
||||||
|
const [manifest, setManifest] = useState({ items: [] })
|
||||||
|
const [importedIds, setImportedIds] = useState([])
|
||||||
|
const [libraryRoot, setLibraryRoot] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onMessage('LIBRARY_STATE', ({ manifest, importedIds, libraryRoot }) => {
|
||||||
|
if (manifest) setManifest(manifest)
|
||||||
|
if (importedIds) setImportedIds(importedIds)
|
||||||
|
if (libraryRoot) setLibraryRoot(libraryRoot)
|
||||||
|
})
|
||||||
|
notifyReady()
|
||||||
|
const blockContext = (ev) => ev.preventDefault()
|
||||||
|
document.addEventListener('contextmenu', blockContext)
|
||||||
|
return () => document.removeEventListener('contextmenu', blockContext)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LibraryBrowser
|
||||||
|
manifest={manifest}
|
||||||
|
importedIds={importedIds}
|
||||||
|
libraryRoot={libraryRoot}
|
||||||
|
onImport={(id) => send('IMPORT_ITEM', { id })}
|
||||||
|
onReload={() => send('RELOAD', {})}
|
||||||
|
onClose={() => send('CLOSE', {})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+63
-43
@@ -19,6 +19,7 @@ import {
|
|||||||
toggleReferenzlinien,
|
toggleReferenzlinien,
|
||||||
toggleOsnap,
|
toggleOsnap,
|
||||||
setOsnapMode, toggleGridVisible,
|
setOsnapMode, toggleGridVisible,
|
||||||
|
openProjectSettings,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = [
|
||||||
@@ -275,7 +276,7 @@ export default function OberleisteApp() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openDossierSettings()}
|
onClick={() => openDossierSettings()}
|
||||||
title="Dossier-Einstellungen"
|
title="Dossier-Einstellungen (App-Settings, Window-Layout)"
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent', border: 'none', padding: '2px 4px',
|
background: 'transparent', border: 'none', padding: '2px 4px',
|
||||||
cursor: 'pointer', color: 'var(--text-muted)',
|
cursor: 'pointer', color: 'var(--text-muted)',
|
||||||
@@ -286,6 +287,19 @@ export default function OberleisteApp() {
|
|||||||
>
|
>
|
||||||
<Icon name="settings" size={14} />
|
<Icon name="settings" size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openProjectSettings()}
|
||||||
|
title="Projekt-Einstellungen (Voreinstellungen Geschoss/Schnitt + Material-Library)"
|
||||||
|
style={{
|
||||||
|
background: 'transparent', border: 'none', padding: '2px 4px',
|
||||||
|
cursor: 'pointer', color: 'var(--text-muted)',
|
||||||
|
display: 'flex', alignItems: 'center', flexShrink: 0,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
|
||||||
|
>
|
||||||
|
<Icon name="tune" size={14} />
|
||||||
|
</button>
|
||||||
<div style={sep} />
|
<div style={sep} />
|
||||||
{/* ====== VIEW 2x4 Grid ======
|
{/* ====== VIEW 2x4 Grid ======
|
||||||
Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings)
|
Reihe 1: TOP / ISO / PERSP / 📷 (Kamera-Settings)
|
||||||
@@ -709,16 +723,16 @@ export default function OberleisteApp() {
|
|||||||
<div style={sep} />
|
<div style={sep} />
|
||||||
|
|
||||||
{/* ====== SNAP-BAR (Architektur-Osnaps + Grid) ======
|
{/* ====== SNAP-BAR (Architektur-Osnaps + Grid) ======
|
||||||
4×2 Grid, nur Icons, schlank. Symbol-Wahl orientiert sich an
|
Layout: links zwei einzelne Toggle-Buttons (Master-O / Grid)
|
||||||
Rhinos eigenen Snap-Markern (End=Quadrat, Mid=Dreieck, Cen=
|
stacked. Rechts zwei 3-Zellen-Pills mit den Osnap-Modi.
|
||||||
Kreis, Int=X, Perp=Winkel, Near=Plus). Ortho + Grid-Snap sind
|
Master-O und Grid sind globale Toggles → eigene Buttons.
|
||||||
in Rhinos Footer-Bar — hier nur was dort fehlt.
|
Die 6 Modi gehören zusammen → Pill-Gruppe.
|
||||||
Reihe 1: Master-O | End | Mid | Int
|
Ortho + Grid-Snap sind in Rhinos Footer-Bar — hier nicht. */}
|
||||||
Reihe 2: Perp | Cen | Near | Grid */}
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const om = state.osnapModes || {}
|
const om = state.osnapModes || {}
|
||||||
const osnapDisabled = !state.osnap
|
const osnapDisabled = !state.osnap
|
||||||
const IconBtn = ({ icon, active, disabled, onClick, isFirst, title }) => (
|
// Bar-Cell: Teil einer Segment-Pill (kein eigener Border-Radius)
|
||||||
|
const BarCell = ({ icon, active, disabled, onClick, isFirst, title }) => (
|
||||||
<button onClick={onClick} disabled={disabled} title={title}
|
<button onClick={onClick} disabled={disabled} title={title}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (disabled || active) return
|
if (disabled || active) return
|
||||||
@@ -746,53 +760,59 @@ export default function OberleisteApp() {
|
|||||||
<Icon name={icon} size={12} />
|
<Icon name={icon} size={12} />
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
const rowStyle = {
|
const pillRowStyle = {
|
||||||
display: 'inline-flex', width: BAR_H * 4,
|
display: 'inline-flex', width: BAR_H * 3,
|
||||||
height: BAR_H + 2, boxSizing: 'border-box',
|
height: BAR_H + 2, boxSizing: 'border-box',
|
||||||
border: '1px solid var(--border)', borderRadius: 999,
|
border: '1px solid var(--border)', borderRadius: 999,
|
||||||
overflow: 'hidden', flexShrink: 0,
|
overflow: 'hidden', flexShrink: 0,
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ display: 'flex', gap: 6, alignItems: 'flex-start',
|
||||||
display: 'flex', flexDirection: 'column', gap: 4, flexShrink: 0,
|
flexShrink: 0 }}>
|
||||||
}}>
|
{/* Linke Spalte: 2 einzelne Toggle-Buttons stacked */}
|
||||||
<div style={rowStyle}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
<IconBtn icon="gps_fixed" isFirst
|
<BarButton icon="gps_fixed"
|
||||||
active={!!state.osnap}
|
active={!!state.osnap}
|
||||||
onClick={() => toggleOsnap(!state.osnap)}
|
onClick={() => toggleOsnap(!state.osnap)}
|
||||||
title={state.osnap ? 'Object-Snap an' : 'Object-Snap aus'} />
|
title={state.osnap ? 'Object-Snap an — Klick zum Ausschalten'
|
||||||
<IconBtn icon="crop_square" disabled={osnapDisabled}
|
: 'Object-Snap aus — Klick zum Einschalten'} />
|
||||||
active={!!om.end}
|
<BarButton
|
||||||
onClick={() => setOsnapMode('end', !om.end)}
|
|
||||||
title="Endpunkt (End)" />
|
|
||||||
<IconBtn icon="change_history" disabled={osnapDisabled}
|
|
||||||
active={!!om.mid}
|
|
||||||
onClick={() => setOsnapMode('mid', !om.mid)}
|
|
||||||
title="Mittelpunkt (Mid)" />
|
|
||||||
<IconBtn icon="close" disabled={osnapDisabled}
|
|
||||||
active={!!om.int}
|
|
||||||
onClick={() => setOsnapMode('int', !om.int)}
|
|
||||||
title="Schnittpunkt (Intersection)" />
|
|
||||||
</div>
|
|
||||||
<div style={rowStyle}>
|
|
||||||
<IconBtn icon="square_foot" isFirst disabled={osnapDisabled}
|
|
||||||
active={!!om.perp}
|
|
||||||
onClick={() => setOsnapMode('perp', !om.perp)}
|
|
||||||
title="Lotrecht (Perpendicular)" />
|
|
||||||
<IconBtn icon="radio_button_unchecked" disabled={osnapDisabled}
|
|
||||||
active={!!om.cen}
|
|
||||||
onClick={() => setOsnapMode('cen', !om.cen)}
|
|
||||||
title="Kreis-/Bogen-Mittelpunkt (Center)" />
|
|
||||||
<IconBtn icon="add" disabled={osnapDisabled}
|
|
||||||
active={!!om.near}
|
|
||||||
onClick={() => setOsnapMode('near', !om.near)}
|
|
||||||
title="Naechster Punkt (Near)" />
|
|
||||||
<IconBtn
|
|
||||||
icon={state.gridVisible === false ? 'grid_off' : 'grid_on'}
|
icon={state.gridVisible === false ? 'grid_off' : 'grid_on'}
|
||||||
active={state.gridVisible !== false}
|
active={state.gridVisible !== false}
|
||||||
onClick={() => toggleGridVisible(state.gridVisible === false)}
|
onClick={() => toggleGridVisible(state.gridVisible === false)}
|
||||||
title="Konstruktions-Raster ein-/ausblenden" />
|
title="Konstruktions-Raster ein-/ausblenden" />
|
||||||
</div>
|
</div>
|
||||||
|
{/* Rechte Spalte: 2 Pills mit den 6 Osnap-Modi */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<div style={pillRowStyle}>
|
||||||
|
<BarCell icon="crop_square" isFirst disabled={osnapDisabled}
|
||||||
|
active={!!om.end}
|
||||||
|
onClick={() => setOsnapMode('end', !om.end)}
|
||||||
|
title="Endpunkt (End)" />
|
||||||
|
<BarCell icon="change_history" disabled={osnapDisabled}
|
||||||
|
active={!!om.mid}
|
||||||
|
onClick={() => setOsnapMode('mid', !om.mid)}
|
||||||
|
title="Mittelpunkt (Mid)" />
|
||||||
|
<BarCell icon="close" disabled={osnapDisabled}
|
||||||
|
active={!!om.int}
|
||||||
|
onClick={() => setOsnapMode('int', !om.int)}
|
||||||
|
title="Schnittpunkt (Intersection)" />
|
||||||
|
</div>
|
||||||
|
<div style={pillRowStyle}>
|
||||||
|
<BarCell icon="square_foot" isFirst disabled={osnapDisabled}
|
||||||
|
active={!!om.perp}
|
||||||
|
onClick={() => setOsnapMode('perp', !om.perp)}
|
||||||
|
title="Lotrecht (Perpendicular)" />
|
||||||
|
<BarCell icon="radio_button_unchecked" disabled={osnapDisabled}
|
||||||
|
active={!!om.cen}
|
||||||
|
onClick={() => setOsnapMode('cen', !om.cen)}
|
||||||
|
title="Kreis-/Bogen-Mittelpunkt (Center)" />
|
||||||
|
<BarCell icon="add" disabled={osnapDisabled}
|
||||||
|
active={!!om.near}
|
||||||
|
onClick={() => setOsnapMode('near', !om.near)}
|
||||||
|
title="Naechster Punkt (Near)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import ProjectSettingsDialog from './components/ProjectSettingsDialog'
|
||||||
|
import { notifyReady } from './lib/rhinoBridge'
|
||||||
|
|
||||||
|
function bridgeSend(type, payload = {}) {
|
||||||
|
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||||
|
const json = JSON.stringify({ type, payload })
|
||||||
|
document.title = 'RHINOMSG::' + json
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectSettingsApp() {
|
||||||
|
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
notifyReady()
|
||||||
|
const blockContext = (ev) => ev.preventDefault()
|
||||||
|
document.addEventListener('contextmenu', blockContext)
|
||||||
|
return () => document.removeEventListener('contextmenu', blockContext)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectSettingsDialog
|
||||||
|
embedded
|
||||||
|
initial={initial}
|
||||||
|
onSave={(updated) => bridgeSend('SAVE', updated)}
|
||||||
|
onClose={() => bridgeSend('CANCEL', {})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,9 +28,11 @@ export default function ZeichnungsebenenApp() {
|
|||||||
const [activeId, setActiveId] = useState('eg')
|
const [activeId, setActiveId] = useState('eg')
|
||||||
const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN)
|
const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN)
|
||||||
const [zMode, setZMode] = useState('active')
|
const [zMode, setZMode] = useState('active')
|
||||||
|
const [projectSettings, setProjectSettings] = useState(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMessage('STATE_SYNC', ({ zeichnungsebenen: z }) => {
|
onMessage('STATE_SYNC', ({ zeichnungsebenen: z, projectSettings: ps }) => {
|
||||||
|
if (ps) setProjectSettings(ps)
|
||||||
if (z) {
|
if (z) {
|
||||||
const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r)
|
const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r)
|
||||||
const active = r.find(zz => zz.id === activeId) || r[0]
|
const active = r.find(zz => zz.id === activeId) || r[0]
|
||||||
@@ -122,6 +124,7 @@ export default function ZeichnungsebenenApp() {
|
|||||||
recalcOkff={recalcOkff}
|
recalcOkff={recalcOkff}
|
||||||
mode={zMode}
|
mode={zMode}
|
||||||
onModeChange={setZMode}
|
onModeChange={setZMode}
|
||||||
|
projectSettings={projectSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const MODES = [
|
|||||||
export default function GeschossManager({
|
export default function GeschossManager({
|
||||||
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
||||||
mode, onModeChange,
|
mode, onModeChange,
|
||||||
|
projectSettings,
|
||||||
}) {
|
}) {
|
||||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||||
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
||||||
@@ -254,16 +255,23 @@ export default function GeschossManager({
|
|||||||
return 'Neu'
|
return 'Neu'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project-Default-Hoehe: erst aktives Geschoss, dann erstes Geschoss in
|
// Default-Hoehe: Hierarchie
|
||||||
// der Liste, dann hartcodiert 3.0. So uebernimmt jeder neue Eintrag den
|
// 1. Project-Settings (zentrale Voreinstellung — explizit gesetzt)
|
||||||
// "typischen" Wert des Projekts ohne dass der User irgendwo setzen muss.
|
// 2. Aktives Geschoss (wahrscheinlich gleich vom User gewuenscht)
|
||||||
|
// 3. Erstes Geschoss in der Liste
|
||||||
|
// 4. Hartcodiert 3.0
|
||||||
|
// Project-Settings ueberschreibt alle anderen wenn vorhanden, damit
|
||||||
|
// der User eine zentrale Vorgabe haben kann.
|
||||||
|
const projDefaults = projectSettings?.defaults || {}
|
||||||
const defaultGeschossHoehe = () => {
|
const defaultGeschossHoehe = () => {
|
||||||
|
if (projDefaults.geschossHoehe != null) return projDefaults.geschossHoehe
|
||||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||||
if (act?.isGeschoss && act.hoehe != null) return act.hoehe
|
if (act?.isGeschoss && act.hoehe != null) return act.hoehe
|
||||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.hoehe != null)
|
const first = zeichnungsebenen.find(z => z.isGeschoss && z.hoehe != null)
|
||||||
return first?.hoehe ?? 3.0
|
return first?.hoehe ?? 3.0
|
||||||
}
|
}
|
||||||
const defaultSchnitthoehe = () => {
|
const defaultSchnitthoehe = () => {
|
||||||
|
if (projDefaults.schnitthoehe != null) return projDefaults.schnitthoehe
|
||||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||||
if (act?.isGeschoss && act.schnitthoehe != null) return act.schnitthoehe
|
if (act?.isGeschoss && act.schnitthoehe != null) return act.schnitthoehe
|
||||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.schnitthoehe != null)
|
const first = zeichnungsebenen.find(z => z.isGeschoss && z.schnitthoehe != null)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import Icon from './Icon'
|
||||||
|
|
||||||
|
/* MaterialCard — Preview-Swatch + Name + Tags + Import-Button. */
|
||||||
|
function MaterialCard({ item, imported, onImport }) {
|
||||||
|
const color = item.data?.color || '#888888'
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
border: '1px solid var(--border)', borderRadius: 6,
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: imported ? 'default' : 'pointer',
|
||||||
|
opacity: imported ? 0.75 : 1,
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => { if (!imported) onImport(item.id) }}
|
||||||
|
title={imported ? 'Bereits importiert' : 'Doppelklick = importieren'}>
|
||||||
|
<div style={{
|
||||||
|
height: 64,
|
||||||
|
background: color,
|
||||||
|
borderBottom: '1px solid var(--border-light)',
|
||||||
|
}} />
|
||||||
|
<div style={{ padding: '6px 8px', display: 'flex',
|
||||||
|
flexDirection: 'column', gap: 4 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<span style={{ flex: 1, fontSize: 11, fontWeight: 600,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap' }}>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
{imported && (
|
||||||
|
<Icon name="check" size={12}
|
||||||
|
style={{ color: 'var(--accent)' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(item.tags || []).length > 0 && (
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||||
|
{(item.tags || []).slice(0, 4).map(t => (
|
||||||
|
<span key={t} style={{
|
||||||
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
padding: '1px 5px', borderRadius: 999,
|
||||||
|
}}>{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => { if (!imported) onImport(item.id) }}
|
||||||
|
disabled={imported}
|
||||||
|
style={{
|
||||||
|
marginTop: 2, padding: '3px 6px', fontSize: 10,
|
||||||
|
background: imported ? 'transparent' : 'var(--accent)',
|
||||||
|
color: imported ? 'var(--text-muted)' : '#fff',
|
||||||
|
border: imported ? '1px solid var(--border)' : 'none',
|
||||||
|
borderRadius: 999, cursor: imported ? 'default' : 'pointer',
|
||||||
|
}}>
|
||||||
|
{imported ? 'Importiert' : 'Importieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LibraryBrowser({
|
||||||
|
manifest, importedIds, libraryRoot,
|
||||||
|
onImport, onReload, onClose,
|
||||||
|
}) {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [typeFilter, setTypeFilter] = useState('all')
|
||||||
|
|
||||||
|
const items = manifest?.items || []
|
||||||
|
const importedSet = useMemo(() => new Set(importedIds || []),
|
||||||
|
[importedIds])
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = search.trim().toLowerCase()
|
||||||
|
return items.filter(it => {
|
||||||
|
if (typeFilter !== 'all' && it.type !== typeFilter) return false
|
||||||
|
if (!q) return true
|
||||||
|
if ((it.name || '').toLowerCase().includes(q)) return true
|
||||||
|
if ((it.tags || []).some(t => t.toLowerCase().includes(q))) return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}, [items, search, typeFilter])
|
||||||
|
|
||||||
|
const types = useMemo(() => {
|
||||||
|
const s = new Set(items.map(i => i.type))
|
||||||
|
return ['all', ...Array.from(s)]
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: '100%', height: '100%',
|
||||||
|
background: 'var(--bg-dialog)',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<Icon name="inventory_2" size={14}
|
||||||
|
style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||||
|
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
|
||||||
|
{manifest?.name || 'Dossier-Library'}
|
||||||
|
</span>
|
||||||
|
<button onClick={onReload}
|
||||||
|
title="Manifest neu laden"
|
||||||
|
style={{ background: 'transparent', border: 'none',
|
||||||
|
cursor: 'pointer', color: 'var(--text-muted)',
|
||||||
|
padding: '0 4px' }}>
|
||||||
|
<Icon name="refresh" size={14} />
|
||||||
|
</button>
|
||||||
|
<button onClick={onClose}
|
||||||
|
style={{ color: 'var(--text-muted)', fontSize: 16,
|
||||||
|
padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<input type="text" value={search}
|
||||||
|
onChange={(ev) => setSearch(ev.target.value)}
|
||||||
|
placeholder="Suchen (Name oder Tag)…"
|
||||||
|
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
|
||||||
|
borderRadius: 999 }} />
|
||||||
|
<div style={{ display: 'flex', gap: 0,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999, overflow: 'hidden' }}>
|
||||||
|
{types.map(t => (
|
||||||
|
<button key={t}
|
||||||
|
onClick={() => setTypeFilter(t)}
|
||||||
|
style={{
|
||||||
|
background: typeFilter === t ? 'var(--accent)' : 'transparent',
|
||||||
|
color: typeFilter === t ? '#fff' : 'var(--text-muted)',
|
||||||
|
border: 'none', padding: '4px 10px', fontSize: 10,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}>
|
||||||
|
{t === 'all' ? 'Alle' : t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
||||||
|
padding: '10px 12px' }}>
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div style={{ padding: 24, textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||||
|
{items.length === 0
|
||||||
|
? 'Library ist leer. Manifest unter:'
|
||||||
|
: 'Nichts gefunden — Filter aendern?'}
|
||||||
|
{items.length === 0 && libraryRoot && (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 9, fontFamily: 'monospace' }}>
|
||||||
|
{libraryRoot}/library.json
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
||||||
|
gap: 8,
|
||||||
|
}}>
|
||||||
|
{filtered.map(it => (
|
||||||
|
<MaterialCard key={it.id}
|
||||||
|
item={it}
|
||||||
|
imported={importedSet.has(it.id)}
|
||||||
|
onImport={onImport} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
}}>
|
||||||
|
<span>{filtered.length} / {items.length} Items</span>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<span title={libraryRoot}
|
||||||
|
style={{ fontFamily: 'monospace',
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap', maxWidth: 320 }}>
|
||||||
|
{libraryRoot || ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Icon from './Icon'
|
||||||
|
import { openLibrary } from '../lib/rhinoBridge'
|
||||||
|
|
||||||
|
/* Inline Field + Tab helpers fuer kompaktes Layout im Satellite. */
|
||||||
|
function Field({ label, hint, children, style }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3,
|
||||||
|
padding: '5px 0', ...style }}>
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)',
|
||||||
|
fontWeight: 500, letterSpacing: 0.2 }}>{label}</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
|
||||||
|
{hint && (
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
||||||
|
{hint}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabBar({ tabs, active, onChange }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', gap: 0,
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
paddingLeft: 4,
|
||||||
|
}}>
|
||||||
|
{tabs.map(t => (
|
||||||
|
<button key={t.key}
|
||||||
|
onClick={() => onChange(t.key)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: active === t.key ? '2px solid var(--accent)' : '2px solid transparent',
|
||||||
|
padding: '8px 14px',
|
||||||
|
fontSize: 11, fontWeight: active === t.key ? 600 : 500,
|
||||||
|
color: active === t.key ? 'var(--text-primary)' : 'var(--text-muted)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginBottom: -1,
|
||||||
|
}}>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid', gridTemplateColumns: '14px 1fr 90px 60px 18px',
|
||||||
|
alignItems: 'center', gap: 6,
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderBottom: '1px solid var(--border-light)',
|
||||||
|
background: builtin ? 'var(--bg-section)' : 'transparent',
|
||||||
|
}}>
|
||||||
|
<input type="color" value={mat.color || '#888888'}
|
||||||
|
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
||||||
|
title="Farbe"
|
||||||
|
style={{ width: 14, height: 14, padding: 0, border: 'none',
|
||||||
|
background: 'transparent', cursor: 'pointer' }} />
|
||||||
|
<input type="text" value={mat.name || ''}
|
||||||
|
onChange={(ev) => onChange({ ...mat, name: ev.target.value })}
|
||||||
|
disabled={builtin}
|
||||||
|
placeholder="Name"
|
||||||
|
style={{ flex: 1, fontSize: 11, minWidth: 0,
|
||||||
|
opacity: builtin ? 0.7 : 1 }} />
|
||||||
|
<select value={mat.hatch || 'Solid'}
|
||||||
|
onChange={(ev) => onChange({ ...mat, hatch: ev.target.value })}
|
||||||
|
style={{ fontSize: 11, minWidth: 0 }}>
|
||||||
|
{(hatchPatterns || []).map(h => (
|
||||||
|
<option key={h} value={h}>{h}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input type="number" step="0.1" min="0.1"
|
||||||
|
value={mat.scale ?? 1.0}
|
||||||
|
onChange={(ev) => onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })}
|
||||||
|
title="Hatch-Skalierung"
|
||||||
|
style={{ fontSize: 11, textAlign: 'right' }} />
|
||||||
|
{mat.source === 'library' ? (
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--accent-light)' }}
|
||||||
|
title="Aus Dossier-Library">L</span>
|
||||||
|
) : builtin || mat.source === 'builtin' ? (
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}
|
||||||
|
title="Eingebaut">B</span>
|
||||||
|
) : (
|
||||||
|
<button onClick={onDelete} title="Loeschen"
|
||||||
|
style={{ background: 'transparent', border: 'none',
|
||||||
|
padding: 0, cursor: 'pointer',
|
||||||
|
color: 'var(--text-muted)' }}>
|
||||||
|
<Icon name="close" size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectSettingsDialog({
|
||||||
|
initial, onSave, onClose, embedded = false,
|
||||||
|
}) {
|
||||||
|
const [tab, setTab] = useState('defaults')
|
||||||
|
const [draft, setDraft] = useState(() => ({
|
||||||
|
defaults: { ...(initial.defaults || {}) },
|
||||||
|
materials: [...(initial.materials || [])],
|
||||||
|
}))
|
||||||
|
const builtin = initial.builtinMaterials || []
|
||||||
|
const hatchPatterns = initial.hatchPatterns || ['Solid']
|
||||||
|
|
||||||
|
const setDefault = (k, v) =>
|
||||||
|
setDraft(d => ({ ...d, defaults: { ...d.defaults, [k]: v } }))
|
||||||
|
|
||||||
|
const setMat = (i, newMat) => setDraft(d => ({
|
||||||
|
...d, materials: d.materials.map((m, idx) => idx === i ? newMat : m),
|
||||||
|
}))
|
||||||
|
const delMat = (i) => setDraft(d => ({
|
||||||
|
...d, materials: d.materials.filter((_, idx) => idx !== i),
|
||||||
|
}))
|
||||||
|
const addMat = () => setDraft(d => ({
|
||||||
|
...d,
|
||||||
|
materials: [...d.materials, {
|
||||||
|
name: 'Neues Material', color: '#aaaaaa',
|
||||||
|
hatch: 'Solid', scale: 1.0,
|
||||||
|
// source/libraryId vorbereitet fuer kommendes Library-Feature:
|
||||||
|
// 'local' = manuell erstellt, 'library' = aus Cloud-Repo, libraryId
|
||||||
|
// referenziert dann das Library-Item per UUID.
|
||||||
|
source: 'local', libraryId: null,
|
||||||
|
}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const wrapperStyle = embedded ? {
|
||||||
|
width: '100%', height: '100%',
|
||||||
|
background: 'var(--bg-dialog)',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
||||||
|
} : {
|
||||||
|
position: 'absolute', inset: 0, zIndex: 150,
|
||||||
|
background: 'var(--bg-overlay)',
|
||||||
|
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||||
|
paddingTop: 40,
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={wrapperStyle}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
||||||
|
minHeight: 0, overflow: 'hidden' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<Icon name="tune" size={14}
|
||||||
|
style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
|
||||||
|
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
|
||||||
|
Projekt-Einstellungen
|
||||||
|
</span>
|
||||||
|
<button onClick={onClose}
|
||||||
|
style={{ color: 'var(--text-muted)', fontSize: 16,
|
||||||
|
padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabBar tabs={[
|
||||||
|
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||||
|
{ key: 'materials', label: 'Materialien' },
|
||||||
|
]} active={tab} onChange={setTab} />
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
||||||
|
padding: '8px 14px' }}>
|
||||||
|
{tab === 'defaults' && (
|
||||||
|
<>
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
||||||
|
Diese Werte werden beim Erstellen neuer Elemente als
|
||||||
|
Voreinstellung genommen. Pro-Element editierte Werte
|
||||||
|
bleiben davon unberuehrt.
|
||||||
|
</div>
|
||||||
|
<Field label="STANDARD-GESCHOSSHÖHE (m)"
|
||||||
|
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
|
||||||
|
<input type="number" step="0.05" min="1.0" max="10"
|
||||||
|
value={draft.defaults.geschossHoehe ?? 3.0}
|
||||||
|
onChange={(ev) => setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)}
|
||||||
|
style={{ flex: 1, textAlign: 'right' }} />
|
||||||
|
</Field>
|
||||||
|
<Field label="STANDARD-SCHNITTHÖHE (m)"
|
||||||
|
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses">
|
||||||
|
<input type="number" step="0.05" min="0.1" max="3"
|
||||||
|
value={draft.defaults.schnitthoehe ?? 1.0}
|
||||||
|
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
||||||
|
style={{ flex: 1, textAlign: 'right' }} />
|
||||||
|
</Field>
|
||||||
|
<div style={{ height: 1, background: 'var(--border-light)',
|
||||||
|
margin: '8px 0' }} />
|
||||||
|
<Field label="STANDARD-SCHNITT-TIEFE HINTEN (m)"
|
||||||
|
hint="Default-Tiefe für neue Schnitte/Ansichten — wie weit hinter der Schnittlinie sichtbar">
|
||||||
|
<input type="number" step="0.5" min="0.5"
|
||||||
|
value={draft.defaults.schnittDepthBack ?? 8.0}
|
||||||
|
onChange={(ev) => setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)}
|
||||||
|
style={{ flex: 1, textAlign: 'right' }} />
|
||||||
|
</Field>
|
||||||
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
|
<Field label="HÖHE UNTEN (m)" style={{ flex: 1 }}>
|
||||||
|
<input type="number" step="0.1"
|
||||||
|
value={draft.defaults.schnittHeightMin ?? -1.0}
|
||||||
|
onChange={(ev) => setDefault('schnittHeightMin', parseFloat(ev.target.value))}
|
||||||
|
style={{ flex: 1, textAlign: 'right' }} />
|
||||||
|
</Field>
|
||||||
|
<Field label="HÖHE OBEN (m)" style={{ flex: 1 }}>
|
||||||
|
<input type="number" step="0.1"
|
||||||
|
value={draft.defaults.schnittHeightMax ?? 12.0}
|
||||||
|
onChange={(ev) => setDefault('schnittHeightMax', parseFloat(ev.target.value))}
|
||||||
|
style={{ flex: 1, textAlign: 'right' }} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'materials' && (
|
||||||
|
<>
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
||||||
|
Eingebaute Materialien (B) koennen nicht umbenannt werden,
|
||||||
|
aber Farbe + Hatch sind anpassbar. Eigene Materialien
|
||||||
|
koennen frei angelegt werden.
|
||||||
|
</div>
|
||||||
|
{/* Built-in materials (read-only name) */}
|
||||||
|
{builtin.map((m, i) => (
|
||||||
|
<MaterialRow key={'b_' + m.name}
|
||||||
|
mat={m}
|
||||||
|
builtin
|
||||||
|
hatchPatterns={hatchPatterns}
|
||||||
|
onChange={() => {/* read-only fuer Phase 1 */}} />
|
||||||
|
))}
|
||||||
|
{/* User materials */}
|
||||||
|
{draft.materials.map((m, i) => (
|
||||||
|
<MaterialRow key={'u_' + i}
|
||||||
|
mat={m}
|
||||||
|
hatchPatterns={hatchPatterns}
|
||||||
|
onChange={(nm) => setMat(i, nm)}
|
||||||
|
onDelete={() => delMat(i)} />
|
||||||
|
))}
|
||||||
|
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
|
||||||
|
<button onClick={addMat}
|
||||||
|
style={{
|
||||||
|
padding: '4px 10px', fontSize: 11,
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999, cursor: 'pointer',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||||
|
}}>
|
||||||
|
<Icon name="add" size={12} /> Material hinzufuegen
|
||||||
|
</button>
|
||||||
|
<button onClick={openLibrary}
|
||||||
|
title="Material aus Dossier-Library importieren"
|
||||||
|
style={{
|
||||||
|
padding: '4px 10px', fontSize: 11,
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--accent)',
|
||||||
|
borderRadius: 999, cursor: 'pointer',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||||
|
}}>
|
||||||
|
<Icon name="inventory_2" size={12} /> Library oeffnen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||||
|
<button className="btn-contained" onClick={() => onSave(draft)}>
|
||||||
|
Übernehmen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -185,6 +185,10 @@ export function toggleReferenzlinien(visible) {
|
|||||||
// toggleOrtho/toggleGridSnap/toggleOsnap existieren bereits weiter oben.
|
// toggleOrtho/toggleGridSnap/toggleOsnap existieren bereits weiter oben.
|
||||||
export function setOsnapMode(key, on) { send('SET_OSNAP_MODE', { key, enabled: !!on }) }
|
export function setOsnapMode(key, on) { send('SET_OSNAP_MODE', { key, enabled: !!on }) }
|
||||||
export function toggleGridVisible(on) { send('TOGGLE_GRID_VISIBLE', { visible: !!on }) }
|
export function toggleGridVisible(on) { send('TOGGLE_GRID_VISIBLE', { visible: !!on }) }
|
||||||
|
// Projekt-Einstellungen (Voreinstellungen fuer Geschoss/Schnitt + Material-Library)
|
||||||
|
export function openProjectSettings() { send('OPEN_PROJECT_SETTINGS', {}) }
|
||||||
|
// Dossier-Library (Material-/Symbol-/Object-Templates, Phase A: lokal+material)
|
||||||
|
export function openLibrary() { send('OPEN_LIBRARY', {}) }
|
||||||
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
// 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 }
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import './index.css'
|
|||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
|
import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
|
||||||
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
|
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
|
||||||
|
import ProjectSettingsApp from './ProjectSettingsApp.jsx'
|
||||||
|
import LibraryApp from './LibraryApp.jsx'
|
||||||
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
|
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
|
||||||
import GeschossDialogApp from './GeschossDialogApp.jsx'
|
import GeschossDialogApp from './GeschossDialogApp.jsx'
|
||||||
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
|
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
|
||||||
@@ -39,6 +41,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'elemente' ? ElementeApp
|
: mode === 'elemente' ? ElementeApp
|
||||||
: mode === 'zeichnungsebenen' ? ZeichnungsebenenApp
|
: mode === 'zeichnungsebenen' ? ZeichnungsebenenApp
|
||||||
: mode === 'geschoss_settings' ? GeschossSettingsApp
|
: mode === 'geschoss_settings' ? GeschossSettingsApp
|
||||||
|
: mode === 'project_settings' ? ProjectSettingsApp
|
||||||
|
: mode === 'library' ? LibraryApp
|
||||||
: mode === 'ebenen_settings' ? EbenenSettingsApp
|
: mode === 'ebenen_settings' ? EbenenSettingsApp
|
||||||
: mode === 'geschoss_dialog' ? GeschossDialogApp
|
: mode === 'geschoss_dialog' ? GeschossDialogApp
|
||||||
: mode === 'layer_combinations' ? LayerCombinationsApp
|
: mode === 'layer_combinations' ? LayerCombinationsApp
|
||||||
|
|||||||
Reference in New Issue
Block a user