diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py
index 7a5215e..98a34df 100644
--- a/rhino/rhinopanel.py
+++ b/rhino/rhinopanel.py
@@ -49,6 +49,154 @@ def _hatch_pattern_names(doc):
return out
+def _list_hatch_patterns_full(doc):
+ """Vollstaendiges Hatch-Pattern-Listing fuer die Verwaltungs-UI.
+ Inkludiert die HatchLines (angle, base, offset, dashes) damit
+ Frontend echte Pattern-Previews rendern kann statt Platzhalter."""
+ out = []
+ try:
+ for i in range(doc.HatchPatterns.Count):
+ try:
+ hp = doc.HatchPatterns[i]
+ if hp is None: continue
+ try:
+ if bool(hp.IsDeleted): continue
+ except Exception: pass
+ ftype = "Lines"
+ try:
+ ft = hp.FillType
+ ftype = str(ft).split(".")[-1]
+ except Exception: pass
+ ref = 0
+ try: ref = int(hp.Reference)
+ except Exception: pass
+ desc = ""
+ try: desc = str(hp.Description) or ""
+ except Exception: pass
+ # HatchLines extrahieren (nur Lines-Typ)
+ hlines = []
+ if ftype.lower() == "lines":
+ try:
+ # Probiere zuerst die Property, dann GetHatchLines()
+ lines = None
+ try: lines = hp.HatchLines
+ except Exception: pass
+ if lines is None:
+ try: lines = hp.GetHatchLines()
+ except Exception: pass
+ if lines is None:
+ print("[EBENEN] hp[{}] '{}' HatchLines=None".format(
+ i, hp.Name))
+ else:
+ try: cnt = len(lines)
+ except Exception:
+ try: cnt = lines.Count
+ except Exception: cnt = -1
+ print("[EBENEN] hp[{}] '{}' HatchLines type={} count={}".format(
+ i, hp.Name, type(lines).__name__, cnt))
+ for hl in lines:
+ try:
+ bp = hl.BasePoint
+ off = hl.Offset
+ # Dashes optional — Property heisst auf
+ # manchen Rhino-Versionen GetDashes() oder
+ # DashCount/GetDash(i)
+ dashes = []
+ try:
+ dr = hl.Dashes
+ if dr is not None:
+ for d in dr: dashes.append(float(d))
+ except Exception:
+ # Versuche GetDashes()
+ try:
+ dr = hl.GetDashes()
+ if dr is not None:
+ for d in dr: dashes.append(float(d))
+ except Exception:
+ # Versuche DashCount + GetDash(i)
+ try:
+ dc = int(hl.DashCount)
+ for k in range(dc):
+ dashes.append(float(hl.GetDash(k)))
+ except Exception: pass
+ entry = {
+ "angle": float(hl.Angle),
+ "baseX": float(bp.X),
+ "baseY": float(bp.Y),
+ "offX": float(off.X),
+ "offY": float(off.Y),
+ "dashes": dashes,
+ }
+ hlines.append(entry)
+ except Exception as ex:
+ print("[EBENEN] hp[{}] hl FAIL:".format(i), ex)
+ except Exception as ex:
+ print("[EBENEN] hp[{}] HatchLines outer FAIL:".format(i), ex)
+ out.append({
+ "index": i,
+ "name": hp.Name or "",
+ "fillType": ftype,
+ "description": desc,
+ "isReference": (ref > 0),
+ "hatchLines": hlines,
+ })
+ except Exception:
+ continue
+ except Exception as ex:
+ print("[EBENEN] _list_hatch_patterns_full:", ex)
+ return out
+
+
+def _list_linetypes_full(doc):
+ """Vollstaendiges Linetype-Listing fuer die Verwaltungs-UI.
+ Mac Rhino 8 GetSegment(i) returnt (length: float, isLine: bool):
+ True = Line-Segment, False = Space (Gap). Bei Dot ist length=0.0
+ + isLine=True (Punkt = unendlich kurzer Strich)."""
+ out = []
+ try:
+ for i in range(doc.Linetypes.Count):
+ try:
+ lt = doc.Linetypes[i]
+ if lt is None or lt.IsDeleted: continue
+ segs = []
+ sc_cnt = 0
+ try: sc_cnt = int(lt.SegmentCount)
+ except Exception: pass
+ for s in range(sc_cnt):
+ try:
+ seg = lt.GetSegment(s)
+ if seg is None: continue
+ length = float(seg[0])
+ # seg[1]: True = Line/Dot, False = Space/Gap
+ is_line = bool(seg[1])
+ if is_line and length == 0.0:
+ stype = "Dot"
+ elif is_line:
+ stype = "Line"
+ else:
+ stype = "Space"
+ segs.append({"length": length, "type": stype})
+ except Exception as ex:
+ print("[EBENEN] lt[{}] GetSegment({}) FAIL: {}".format(
+ i, s, ex))
+ ref = 0
+ try: ref = int(lt.Reference)
+ except Exception: pass
+ out.append({
+ "index": i,
+ "name": lt.Name or "",
+ "segments": segs,
+ "isContinuous": (len(segs) == 0),
+ "isReference": (ref > 0),
+ })
+ except Exception as ex:
+ print("[EBENEN] linetype outer FAIL:", ex)
+ continue
+ except Exception as ex:
+ print("[EBENEN] _list_linetypes_full:", ex)
+ return out
+
+
def _read_launcher_schema():
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
@@ -692,6 +840,8 @@ class EbenenBridge(panel_base.BaseBridge):
"materials": current.get("materials", []),
"builtinMaterials": built_in,
"hatchPatterns": _hatch_pattern_names(doc),
+ "hatchPatternsFull": _list_hatch_patterns_full(doc),
+ "linetypes": _list_linetypes_full(doc),
}
def on_save(updated):
doc2 = Rhino.RhinoDoc.ActiveDoc
@@ -774,6 +924,20 @@ class EbenenBridge(panel_base.BaseBridge):
except Exception: pass
elif t == "PICK_TEXTURE_FILE":
self._pick_texture(p)
+ elif t == "RENAME_LINETYPE":
+ self._rename_linetype(p)
+ elif t == "DELETE_LINETYPE":
+ self._delete_linetype(p)
+ elif t == "LOAD_LINETYPE_DEFAULTS":
+ self._load_linetype_defaults()
+ elif t == "IMPORT_LINETYPE_FILE":
+ self._import_linetype_file()
+ elif t == "RENAME_HATCH":
+ self._rename_hatch(p)
+ elif t == "DELETE_HATCH":
+ self._delete_hatch(p)
+ elif t == "IMPORT_HATCH_FILE":
+ self._import_hatch_file()
def _pick_texture(self, payload):
slot = payload.get("slot") or "diffuse"
try:
@@ -794,6 +958,151 @@ class EbenenBridge(panel_base.BaseBridge):
except Exception as ex:
print("[PROJECT-SETTINGS] pick_texture:", ex)
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
+
+ # ---- Linetype CRUD ----
+ def _rename_linetype(self, payload):
+ idx = payload.get("index")
+ new_name = (payload.get("name") or "").strip()
+ if idx is None or not new_name: return
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ lt = d.Linetypes[int(idx)]
+ if lt is None or lt.IsDeleted: return
+ lt.Name = new_name
+ d.Linetypes.Modify(lt, int(idx), True)
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] rename_linetype:", ex)
+ self._send_tables()
+
+ def _delete_linetype(self, payload):
+ idx = payload.get("index")
+ if idx is None: return
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ lt = d.Linetypes[int(idx)]
+ if lt is None: return
+ # Default-Linetypes (Continuous, ByLayer) sollten nicht
+ # geloescht werden — Rhino's Delete macht es uns aber
+ # einfach: gibt False zurueck wenn nicht erlaubt.
+ d.Linetypes.Delete(int(idx), True)
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] delete_linetype:", ex)
+ self._send_tables()
+
+ def _load_linetype_defaults(self):
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ d.Linetypes.LoadDefaultLinetypes(True)
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] load_linetype_defaults:", ex)
+ self._send_tables()
+
+ def _import_linetype_file(self):
+ """Datei-Picker fuer .lin (AutoCAD-Linetype) → Linetypes.Load."""
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ import Eto.Forms as forms
+ dlg = forms.OpenFileDialog()
+ dlg.Title = "Linientyp-Datei waehlen (.lin)"
+ dlg.MultiSelect = False
+ dlg.Filters.Add(forms.FileFilter("AutoCAD Linetypes", ".lin"))
+ dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
+ parent = bridge_holder.get("form")
+ res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None)
+ if str(res) != "Ok": return
+ path = dlg.FileName or ""
+ if not path: return
+ cnt_before = d.Linetypes.Count
+ # Rhino-API: Linetypes.Load(filename) — Achtung in
+ # manchen Builds heisst es LoadLinetypeFile(...).
+ ok = False
+ try:
+ ok = bool(d.Linetypes.Load(path))
+ except Exception:
+ try: ok = bool(d.Linetypes.LoadLinetypeFile(path))
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] Linetypes.Load:", ex)
+ cnt_after = d.Linetypes.Count
+ print("[PROJECT-SETTINGS] linetype import: ok={} {} -> {} ({} neu)".format(
+ ok, cnt_before, cnt_after, cnt_after - cnt_before))
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] import_linetype_file:", ex)
+ self._send_tables()
+
+ def _import_hatch_file(self):
+ """Datei-Picker fuer .pat (AutoCAD-Hatch) →
+ HatchPatterns.LoadFromFile."""
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ import Eto.Forms as forms
+ dlg = forms.OpenFileDialog()
+ dlg.Title = "Schraffur-Datei waehlen (.pat)"
+ dlg.MultiSelect = False
+ dlg.Filters.Add(forms.FileFilter("AutoCAD Hatches", ".pat"))
+ dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
+ parent = bridge_holder.get("form")
+ res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None)
+ if str(res) != "Ok": return
+ path = dlg.FileName or ""
+ if not path: return
+ cnt_before = d.HatchPatterns.Count
+ cnt_imported = 0
+ try:
+ # LoadFromFile(file, replaceExisting) — returnt int
+ cnt_imported = int(d.HatchPatterns.LoadFromFile(path, False))
+ except Exception:
+ try:
+ cnt_imported = int(d.HatchPatterns.Load(path))
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] HatchPatterns.Load:", ex)
+ cnt_after = d.HatchPatterns.Count
+ print("[PROJECT-SETTINGS] hatch import: {} (Tabelle {} -> {})".format(
+ cnt_imported, cnt_before, cnt_after))
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] import_hatch_file:", ex)
+ self._send_tables()
+
+ # ---- Hatch-Pattern CRUD ----
+ def _rename_hatch(self, payload):
+ idx = payload.get("index")
+ new_name = (payload.get("name") or "").strip()
+ if idx is None or not new_name: return
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ hp = d.HatchPatterns[int(idx)]
+ if hp is None or hp.IsDeleted: return
+ hp.Name = new_name
+ d.HatchPatterns.Modify(hp, int(idx), True)
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] rename_hatch:", ex)
+ self._send_tables()
+
+ def _delete_hatch(self, payload):
+ idx = payload.get("index")
+ if idx is None: return
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ try:
+ d.HatchPatterns.Delete(int(idx), True)
+ except Exception as ex:
+ print("[PROJECT-SETTINGS] delete_hatch:", ex)
+ self._send_tables()
+
+ def _send_tables(self):
+ """Sendet aktuelle Linetype + Hatch-Tabellen ans Frontend
+ (TABLES_UPDATED-Message). Frontend re-rendert die Listen."""
+ d = Rhino.RhinoDoc.ActiveDoc
+ if d is None: return
+ self.send("TABLES_UPDATED", {
+ "linetypes": _list_linetypes_full(d),
+ "hatchPatternsFull": _list_hatch_patterns_full(d),
+ })
b = _ProjectSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"project_settings",
diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx
index f075c20..737c616 100644
--- a/src/components/ProjectSettingsDialog.jsx
+++ b/src/components/ProjectSettingsDialog.jsx
@@ -1,7 +1,11 @@
import { useState, useEffect } from 'react'
import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls'
-import { openLibrary, pickTextureFile, onMessage } from '../lib/rhinoBridge'
+import {
+ openLibrary, pickTextureFile, onMessage,
+ renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile,
+ renameHatch, deleteHatch, importHatchFile,
+} from '../lib/rhinoBridge'
/* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */
function Field({ label, hint, children, style }) {
@@ -67,6 +71,248 @@ function TabBar({ tabs, active, onChange }) {
)
}
+/* LinetypePreview — SVG-Linie mit Strich-Segmenten. segments = [{length,type}]
+ type ∈ Line/Space (manchmal auch Continuous-Ableitungen). Width in px;
+ wir skalieren die Segmente damit das Gesamtmuster in width passt. */
+function LinetypePreview({ segments, width = 120, height = 12 }) {
+ if (!segments || segments.length === 0) {
+ return (
+
+ )
+ }
+ // Dot hat length=0 — fuer Layout-Zwecke kleinen Pseudo-Wert
+ const DOT_DRAW_LEN = 0.6
+ const lenOf = (seg) => {
+ const l = Math.abs(seg.length || 0)
+ return seg.type === 'Dot' ? DOT_DRAW_LEN : l
+ }
+ const patternLen = segments.reduce((s, seg) => s + lenOf(seg), 0)
+ if (patternLen <= 0) {
+ return (
+
+ )
+ }
+ // Statt das Pattern auf die ganze Breite zu strecken: tilen mit fixer
+ // Skala. Ziel: ca. 4-5 Pattern-Repetitions sichtbar, dann ist die
+ // Sequenz auch bei kurzen Patterns (Dots) deutlich erkennbar.
+ const TARGET_REPETITIONS = 4
+ const scale = width / (patternLen * TARGET_REPETITIONS)
+ const scaledPatternLen = patternLen * scale
+ const repetitions = Math.ceil(width / scaledPatternLen) + 1
+ const parts = []
+ let x = 0
+ let key = 0
+ for (let r = 0; r < repetitions && x < width; r++) {
+ for (const seg of segments) {
+ const drawLen = lenOf(seg) * scale
+ if (x >= width) break
+ const x2 = Math.min(x + drawLen, width)
+ if (seg.type === 'Line' && drawLen > 0) {
+ parts.push(